前情提要

Scala函数式编程指南(一) 函数式思想介绍

scala函数式编程(二) scala基础语法介绍

Scala函数式编程(三) scala集合和函数

Scala函数式编程(四)函数式的数据结构 上

Scala函数式编程(四)函数式的数据结构 下

Scala函数式编程(五) 函数式的错误处理

什么时候效率复习最高,毫无疑问是考试前的最后一夜,同样的道理还有寒暑假最后一天做作业最高效。学界有一个定理:deadline是第一生产力,说的就是这个事情。

同样的,这个道理完全可以推广到函数式编程中来,而懒加载(scala的lazy关键字)就是这样的东西。

在函数式编程中,因为要维持不变性,故而需要更多的存储空间,这一点在函数式数据结构中有说到。懒加载可以说会在一定程度上解决这个问题,同时通过缓存数据还能提高一些运行效率,以及通过面向表达式编程提高系统的模块化。

这一节先介绍lazy的具体内容,及其好处,然后通过Stream这一数据结构讨论懒加载更多应用场景以及懒加载是如何实现性能优化的。

1.scala懒加载lazy

1.1 什么是懒加载

懒加载,顾名思义就是一个字懒。就像老板让你去干活,刚叫的时候你不会去干,只有等到着急的时候,催你的时候你才会去干。懒加载就是这样的东西。

我们直接用命令行测试下:

//右边是一个表达式,这里不是懒加载,直接求值
scala> val x = { println("x"); 15 }
x
x: Int = 15 //使用了懒加载,这里和上面的右侧是类似的,不过不会立即求值
scala> lazy val y = { println("y"); 13 }
y: Int = <lazy> //x的值变成15,也就是表达式的结果
scala> x
res2: Int = 15 //懒加载在真正调用的时候,才运行表达式的内容,打印y,并返回值
scala> y
y
res3: Int = 13 //lazy已经缓存的表达式的内容,所以不会再运行表达式里面的东西,也就是表达式内容只运行一次
scala> y
res4: Int = 13

看上面代码就明白了,懒加载就是让表达式里面的计算延迟,并且只计算一次,然后就会缓存结果。

值得一提的是,懒加载只对表达式和函数生效,如果直接定义变量,那是没什么用的。因为懒加载就是让延迟计算,你直接定义变量那计算啥啊。。。

说完lazy这个东西,那就来说说它究竟有什么用。

1.2 懒加载的好处

初次看到这个东西,会疑惑,懒加载有什么用?其实它的用处可不小。

lazy的一个作用,是将推迟复杂的计算,直到需要计算的时候才计算,而如果不使用,则完全不会进行计算。这无疑会提高效率。

而在大量数据的情况下,如果一个计算过程相互依赖,就算后面的计算依赖前面的结果,那么懒加载也可以和缓存计算结合起来,进一步提高计算效率。嗯,有点类似于spark中缓存计算的思想。

除了延迟计算,懒加载也可以用于构建相互依赖或循环的数据结构。我这边再举个从stackOverFlow看到的例子:

这种情况会出现栈溢出,因为无限递归,最终会导致堆栈溢出。

trait Foo { val foo: Foo }
case class Fee extends Foo { val foo = Faa() }
case class Faa extends Foo { val foo = Fee() } println(Fee().foo)
//StackOverflowException

而使用了lazy关键字就不会了,因为经过lazy关键字修饰,变量里面的内容压根就不会去调用。

trait Foo { val foo: Foo }
case class Fee extends Foo { lazy val foo = Faa() }
case class Faa extends Foo { lazy val foo = Fee() } println(Fee().foo)
//Faa()

当然上面这种方法也可以让它全部求值,在后面stream的时候再介绍。

1.3 其他语言的懒加载

看起来懒加载是很神奇的东西,但其实这个玩意也不是什么新鲜东西。一说你可能就会意识到了,其实懒加载就是单例模式中的懒汉构造法。

以下是scala中的懒加载:

class LazyTest {
//懒加载定义一个变量
lazy val msg = "Lazy"
}

如果转成同样功能的java代码:

class LazyTest {
public int bitmap$0;
private String msg; public String msg() {
if ((bitmap$0 & 1) == 0) {
synchronized (this) {
if ((bitmap$0 & 1) == 0) {
synchronized (this) {
msg = "Lazy";
}
}
bitmap$0 = bitmap$0 | 1;
}
}
return msg;
} }

其实说白了,就是考虑多线程情况下,运用懒汉模式创建一个单例的代码。只不过在scala中,提供了语法级别的支持,所以懒加载使用起来更加方便。

OK,介绍完懒加载,我们再说说一个息息相关的数据结构,Stream(流)。

2.Stream数据结构

Stream数据结构,根据名字判断,就知道这是一个流。直观得说,Stream可以看作一个特殊点的List,特殊在于Stream天然就是“懒”的(java8也新增了叫Stream的数据结构,但和scala的还是有点区别的,这一点要区分好)。

直接看代码吧:

//新建List
scala> val li = List(1,2,3,4,5)
li: List[Int] = List(1, 2, 3, 4, 5) //新建Stream
scala> val stream = Stream(1,2,3,4,5)
stream: scala.collection.immutable.Stream[Int] = Stream(1, ?) //每个Stream有两个元素,一个head表示当前元素,tail表示除当前元素后面的其他元素,也可能为空
//就跟链表一样
scala> stream.head
res21: Int = 1 //后一个元素,类似链表
scala> stream.tail
res20: scala.collection.immutable.Stream[Int] = Stream(2, ?)

List可以直接转成Stream,也可以新生成,一个Stream和链表是类似的,有一个当前元素,和一个指向下一个元素的句柄。

但是!Stream不会计算,或者说获取下一个元素的状态和内容。也就是说,在真正调用前,当前是Stream是不知道它指向下一个元素究竟是什么,是不是空的?

那么问题来了,为嘛要大费周章搞这么个Stream?

其实Stream可以做很多事情,这里简单介绍一下。首先说明,无论是懒加载还是Stream,使用它们很大程度是为了提高运行效率或节省空间。

获取数据

Stream特别适合在不确定量级的数据中,获取满足条件的数据。这里给出一个大佬的例子:

Scala中Stream的应用场景及事实上现原理

这个例子讲的是在50个随机数中,获取前3个能被整除的数字。当然直接写个while很简单,但如果要用函数式的方式就不容易了。

而如果要没有一丝一毫的空间浪费,那就只有使用Stream了。

再举个例子,如果要读取一个非常大的文件,要读取第一个'a'字符前面的所有数据。

如果使用getLine或其他iterator的api,那要用循环或递归迭代去获取,而如果用Stream,只需一行代码。

Source.fromFile("path").toStream.takeWhile(_ != 'a')

道理和随机数的那个例子是一样的。

消除中间结果

这是《scala函数式编程》书里面的例子,这里拿来说一说。

有这样一行代码:

List(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).map(_ * 3)

如果让它执行,那么会先执行map方法,生成一个中间结果,再执行filter,返回一个中间结果,再执行map得到最终结果,流程大概如下:

List(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).map(_ * 3) =>
//生成中间结果
List(11,12,13,14).filter(_ % 2 == 0).map(_ * 3) => //又生成中间结果
List(12,14).map(_ * 3) =>
//得到最终结果
List(36,42)

看,上面例子中,会生成多个中间的List,但其实这些是没必要的,我们完全能重写一个While,直接在一个代码块中实现map(_ + 10).filter(_ % 2 == 0).map(_ * 3)这三个函数的功能,但却不够优雅。而Stream能够无缝做到这点。

可以在idea中用代码调试功能追踪一下,因为Stream天生懒的原因,它会让一个元素直接执行全部函数,第一个元素产生结果后,再执行下一个元素,避免中间临时数据产生。看流程:

Stream(1,2,3,4).map(_ + 10).filter(_ % 2 == 0).toList =>
//对第一个元素应用map
Stream(11,Stream(2,3,4)).map(_ + 10).filter(_ % 2 == 0).toList =>
//对第一个元素应用filter
Stream(2,3,4).map(_ + 10).filter(_ % 2 == 0).toList =>
//对第二个元素应用map
Stream(12,Stream(3,4)).map(_ + 10).filter(_ % 2 == 0).toList
//对第二个元素应用filter生成结果
12 :: Stream(3,4).map(_ + 10).filter(_ % 2 == 0).toList => ......以此类推

通过Stream数据结构,可以优雅得去掉临时数据所产生的负面影响。

小结

总而言之,懒加载主要是为了能够在一定程度上提升函数式编程的效率,无论是空间效率还是时间效率。这一点看Stream的各个例子就明白了,Stream这种数据结构天然就是懒的。

同时懒加载更重要的一点是通过分离表达式和值,提升了模块化。这句话听起来比较抽象,还是得看回1.2 懒加载的好处这一节的例子。所谓值和表达式分离,在这个例子中,就是当调用Fee().foo的时候,不会立刻要求得它的值,而只是获得了一个表达式,表达式的值暂时并不关心。这样就将表达式和值分离开来,并且模块化特性更加明显!从这个角度来看,这一点和Scala函数式编程(五) 函数式的错误处理介绍的Try()错误处理有些类似,都是关注表达式而不关注具体的值,其核心归根结底就是为了提升模块化

以上~

Scala函数式编程(六) 懒加载与Stream的更多相关文章

  1. Vue总结第五天:vue-router (使用模块化(创建Vue组件)机制编程)、router-link 标签的属性、路由代码跳转、懒加载、路由嵌套(子路由)、路由传递数据、导航守卫)

    Vue总结第五天:vue-router ✿ 路由(器)目录: □  vue中路由作用 □  vue-router基本使用 □  vue-router嵌套路由 □  vue-router参数传递 □  ...

  2. Scala函数式编程(四)函数式的数据结构 上

    这次来说说函数式的数据结构是什么样子的,本章会先用一个list来举例子说明,最后给出一个Tree数据结构的练习,放在公众号里面,练习里面给出了基本的结构,但代码是空缺的需要补上,此外还有预留的test ...

  3. Scala 基础(十):Scala 函数式编程(二)基础(二)过程、惰性函数、异常

    1 过程 将函数的返回类型为Unit的函数称之为过程(procedure),如果明确函数没有返回值,那么等号可以省略 注意事项和细节说明 1)注意区分: 如果函数声明时没有返回值类型,但是有 = 号, ...

  4. Scala 函数式编程思想

    Spark 选择 Scala 作为开发语言 在 Spark 诞生之初,就有人诟病为什么 AMP 实验室选了一个如此小众的语言 - Scala,很多人还将原因归结为学院派的高冷,但后来事实证明,选择 S ...

  5. Scala函数式编程进阶

    package com.dtspark.scala.basics /** * 函数式编程进阶: * 1,函数和变量一样作为Scala语言的一等公民,函数可以直接赋值给变量: * 2, 函数更长用的方式 ...

  6. 基于javascript实现图片懒加载(亲测有效)

    这篇文章主要介绍了javascript实现图片懒加载的方法及思路,有时我们需要用懒加载,也就是延迟加载图片的方式,来提高网站的亲和力,需要的朋友可以参考下! 一.定义 图片延迟加载也称为懒加载,延迟加 ...

  7. iOS-设计模式-懒加载

    一.为什么要懒加载? 答: iPhone设备内存有限,如果在程序在启动后就一次性加载将来会用到的所有资源,那么久可能会耗尽iOS设备的内存.这些资源例如大量的数据,图片,音频,过多的控件等. 二.懒加 ...

  8. hibernate--lazy(懒加载)属性

    关联映射文件中<class>标签中的lazy(懒加载)属性 Lazy(懒加载):只有在正真使用该对象时,才会创建这个对象 Hibernate中的lazy(懒加载):只有我们在正真使用时,它 ...

  9. Scala-变量、常量和懒加载

    package com.mengyao.scala.function /** * Scala的变量声明和使用(可变类型和值类型) *  * @author mengyao */object Test0 ...

随机推荐

  1. POI小demo

    使用poi需要先下载相关jar包(http://download.csdn.net/detail/wangkunisok/9454545) poi-3.14-20160307.jar poi-ooxm ...

  2. Nginx 入门及基本命令行操作

    Nginx 介绍 Nginx 是一个高性能的 Web 服务器,从 2001 年发展至今,由于 Nginx 对硬件和操作系统内核特性的深度挖掘,使得在保持高并发的同时还能够保持高吞吐量.Nginx 还采 ...

  3. LeetCode | 707. 设计链表

    设计链表的实现.您可以选择使用单链表或双链表.单链表中的节点应该具有两个属性:val 和 next.val 是当前节点的值,next 是指向下一个节点的指针/引用.如果要使用双向链表,则还需要一个属性 ...

  4. 基于Jquery WeUI的微信开发H5页面控件的经验总结(2)

    在微信开发H5页面的时候,往往借助于WeUI或者Jquery WeUI等基础上进行界面效果的开发,由于本人喜欢在Asp.net的Web界面上使用JQuery,因此比较倾向于使用 jQuery WeUI ...

  5. spring的ioc依赖注入的三种方法(xml方式)

    常见的依赖注入方法有三种:构造函数注入.set方法注入.使用P名称空间注入数据.另外说明下注入集合属性 先来说下最常用的那个注入方法吧. 一.set方法注入 顾名思义,就是在类中提供需要注入成员的 s ...

  6. 关于git 远程仓库账户密码错误的问题

    主要是电脑凭证把第一次输入的账户密码记录了下来,在控制面板->用户账户->凭据管理器里, 选择windows凭证, 你会找到git:凭据,直接删掉或者更改都可以! 对应的Git的凭证,删除 ...

  7. Oracle 中文日期转换

    中文日期转换 select to_char(to_date('07-5月-17'),'yyyy-MM-dd HH24:mi:ss') from dual

  8. Consider defining a bean named 'authenticator' in your configuration.

    SpringBoot整合Shiro时出错: 异常日志: o.s.b.d.LoggingFailureAnalysisReporter: *************************** APPL ...

  9. 【洛谷P1801】黑匣子——优先队列

    题目链接 一道有点意思的题目 我们可以维护两个优先队列:pqmin和pqmax 其中 pqmin 是小根堆, pqmax 是大根堆 每次 add 一个数字,则将数字推入到 pqmin 中 每次 get ...

  10. 升级 nop 4.1 Incorrect syntax near 'OFFSET'. Invalid usage of the option NEXT in the FETCH statement.

    Incorrect syntax near 'OFFSET'.  Invalid usage of the option NEXT in the FETCH statement. nop.web 项目 ...