第5章Lambda编程

915

第5章Lambda编程.png

5.1、Lambda表达式和成员引用

lambda表达式,简称lambda,本质上就是可以传递给其他函数的一小段代码,有了lambda,可以轻松的把通用的代码结构抽取成库函数,最常见的一种Lambda用途就是和集合一起工作。

1、Lambda简介:作为函数参数的代码块

  • 在代码中存储和传递一小段行为是常有的任务,例如:当一个事件发生的时候,运行这个事件处理器(比如点击事件),在老版本的Java中,可以用匿名内部类来实现,这种技巧可以工作但是语法太啰嗦了

  • 函数式编程提供了另外一种解决问题的方法:把函数当作值来对待。可以直接传递函数,而不需要先声明一个类再传递这个类的实例

  • 例子

    2fd7173258f8777f173f426c537746a70e4c2c01d2d2d82d7ada83e8dde53c04.png

b8a7273ea0b027ad5e0cc326112dc325cf4de2a0b5fb2d37cb10c5059bd9dcd4.png

2、Lambda和集合

  • 良好编程风格的主要原则之一是避免代码中的任何重复

  • 在没有lambda之前,对集合的操作,已经养成了什么东西都要自己实现的习惯,在Kotlin中这个习惯必须纠正

  • 例子

    • Person类,它包含了这个人的名字和年龄信息

    • 12daba720ece2b01a32e79f6adb182625ca8bb01069e23aff2a188f073109345.png

    • 假设现在有一个人的列表,需要找到列表中年龄最大的那个人

    • 如果完全不了解lambda,需要手动实现这个搜索功能,你可能需要先引入两个中间变量,一个用来保存最大的年龄,而另一个用来保存找到的此年龄的第一个人,然后迭代这个列表,不断更新这两个变量

    • 手动搜索实现

      • 29b10a53dc5c5ae1d5d700cf22a1274e198a4f603de0edca2b6ff644ddbbe88f.png

      • eb3a5153ded3cb84809f37f36b2cfd6e2e54b47d885275d16ed31172165580ca.png

      • 3d970538f7248e2ff43d7c2d2edcd12cef058a121b4e9798f5c336632bf42297.png

    • 使用库函数maxBy实现

      • a38ebd8df01f97d6fb1c1d141e24d3867341036c668e15bd8f8049260a20dc7d.png

      • 2fbaa907ed1039c9eede57b56a05c644776ed7deb1cb8fc0a6dc6332b6c8da9e.png

      • maxBy函数可以在任何集合上调用,且只需要一个实参:一个函数,指定比较哪个值来找到最大元素。花括号的代码{it.age}就是实现了这个逻辑的lambda。它接收一个集合中的元素作为实参(使用it引用它)并且返回用来比较的值

      • 这个例子中,集合元素是Person对象,用来比较的是存储在其age属性中的年龄

      • 如果lambda刚好是函数或者属性的委托,可以用成员引用个替换

        • bdf74a6b1f05a10e5e35cf714d4f9d88eba0a33085561cc1d97dd45f65dcc337.png

3、Lambda表达式的语法

  • 一个lambda把一小段行为进行编码,你能把它当作值到处传递。它可以被独立地声明并存储到一个变量中,但是更常见的还是直接声明它并传递给函数。

  • Lambda表达式的语法

    • a49d2eb163971d2199269ce4ae649f28614be2aa7aa92c5e6684e5722ecde47c.png
    • Kotlin的lambda表达式始终用花括号包围; 注意实参并没有用括号括起来箭头把实参列表和lambda的函数体隔开
  • 可以把lambda表达式存储在一个变量中,把这个变量当做普通函数对待(即通过相应的实参调用它)

    • 16111874b4088d857175e4d7e1154e08e734509f8199eec7c1ab34ec447a695f.png
    • 8a9bafbc554ae50a8eedda2afcb0f8119b324a576e6bde71393a0147430999ba.png
  • 还可以直接调用lambda表达式:

    • 065c6c5294dc0189ca1942bf3836169158b3f3ccda76f6c544228fa553844b38.png

    • 但是这样的语法毫无可读性,也没有什么意义(它等价于直接执行lambda函数体中的代码)

  • 如果确实需要把一小段代码封闭在一个代码块中,可以使用库函数run来执行它的lambda

    • 1e1c3878c360ebf31f4a131bbb58b29572818822f62cb9dee6ce8d91865bb414.png
    • 8b0221dab87a193437bff4acfde75770e10ba6d1e73032d89ff62402ecb36f99.png
  • 查找年龄最大的代码,如果不适用任何简明语法来重写这个例子,代码如下

    • 6313b18c4250effbf74684de511209d6e948ca0bd2b0c821f0ff3fac8f373f7e.png

    • 8040e0cbd78981f27d22b1a471f53df50a239b08f65b87efed6c6a82358e3eba.png

    • 这段代码一目了然:花括号的代码片段是lambda表达式,把它作为实参传给函数,这个lambda接收一个类型为Person的参数并返回它的年龄

    • 但是这段代码有点啰嗦。首先,过多的标点符号破坏了可读性。其次,类型可以从上下文推断出来并可以省略。最后,这种情况下不需要给lambda的参数分配一个名称

    • 让我们来改进这些地方,先拿花括号开刀

    • Kotlin有这样一种语法约定,如果lambda表达式是函数调用的【最后一个实参】,lambda可以放到括号的外边

    • 在这个例子中,lambda是【唯一的实参】,所以可以放到括号的后边

      • 6434bf48931c2d1ae912c8dad020be506bded90f1488ebfa32c367a5f3627bb5.png
    • 当lambda是函数唯一的实参时,可以去掉调用代码中的空括号对

      • 4eb638f65372ebe29680557407cfd59e007e63323f72fe11d3bcd08ef9eb5381.png
    • 三种语法形式含义都是一样的,但最后一种最易读。如果lambda是唯一的实参,可以省略掉括号,而当有多个实参时,既可以把lambda留在括号内来强调它是一个实参,也可以把它放在括号的外面,两种选择都是可行的

    • 如果想传递两个或更多的lambda,不能把超过一个的lambda放到外面,这时使用常规语法来传递它们通常是更好的选择

    • 省略lambda参数类型,和局部变量一样,如果lambda参数的类型可以被推导出来,就不需要显式地指定它

      • 90a7d0a022393252db2d8670174cdfcad4e2659a746c7e327de520f5dc3c181b.png
    • 这里maxBy函数为例,其参数类型始终和集合的元素类型相同,编译器知道你是对一个Person对象的集合调用maxBy函数,所以它能推断lambda参数也会是Person类型

    • 也存在编译器不能推断lambda参数类型的情况,另行讨论。可以遵循这样一条简单的规则:先不声明类型,等编译器报错后再指定他们

    • 可以指定部分实参的类型,而剩下的实参只用名称。如果编译器不能推导出其中一种类型,又或是显式的类型可以提升可读性,这种做法或许更方便

    • 最后简化,使用默认参数名称it代替命名参数。如果当前上下文期望的只有一个参数的lambda且这个参数的类型可以推断出来,就会生成这个名称

      • 9af705a862f3bfd64e7ec969389df0b3e8f864f9a03f521eb43e17a05e409e81.png
      • 代码简化过程 b98b9615a124bf4215058a170c0becd0fd7ae8c89f236ded99e9407d6e06b475.png
    • 仅在实参名称没有显式地指定时这个默认的名称才会生成

    • 注意

      • it约定能大大缩短代码,但不应该滥用它。尤其是在嵌套lambda的情况下,最好显式地声明每个lambda的参数。否则很难搞清楚it引用的到底是哪个值。如果上下文中参数的类型或意义都不是很明朗,显式声明参数的方法也很有效
    • 如果用变量存储lambda,那么就没有可以推断出参数类型的上下文,所以必须显示地指定参数类型

      • 781f6aebb4623cb1a98eac3f5c0cde29b0579f5ddf558adc819292a29d00906e.png
    • 迄今为止,例子都是由单个表达式或语句构成的lambda,其实它可以包含更多的语句

      • d1816a2c5d52a43e2c28df3a33a454aa57150fe10984ac66fc29bdba62778730.png
      • 3160a626d97f687a373d538c636aa13eb213f30576f86fb8be7a0f5ccbfcc71a.png
  • 复杂调用,回顾joinToString函数,Kotlin标准库中也有定义,不过,它可以接收一个附加的函数参数,这个函数可以用toString函数以外的方法来把一个元素转换成字符串

    • 例子

      • 只打印人的名字
      • 6e9e117f4b097040608d6bc7f8b9c1697652bbcdebd88f6c63b1580db8dc69f6.png
      • 09b9001be62221e8025ca8ba428c2ee972d6676e579ad38f5726096772e0433b.png
      • 重写这个调用,把lambda放在括号外
      • 6c90b1767bf8f61ed24b2e4b05daa53f6e9bd11b708c4f73114482b2e8d353d0.png
      • 5fbb316548a1e75b111bcbe6bccd1b88ba83f077a80664482745987af429ab26.png

4、在作用域中访问变量

  • 和lambda表达式形影不离的概念:从上下文中捕捉变量

  • 当在函数内声明一个匿名内部类的时候,能够在这个匿名类内部引用这个函数的参数和局部变量

  • lambda可以做同样的事情,如果在函数内部使用lambda,也可以访问这个函数的参数,还有lambda之前定义的局部变量

  • 例子

    • 我们用标准库函数forEach来展示这种行为

    • 它是最基本的集合操作函数之一:它所做的全部事情就是在集合中的每一个元素上都调用给定的lambda

    • forEach函数比普通的for循环更简洁一些,但它并没有更多其他优势,所以不必把所有的循环换成lambda

    • 下面的代码清单接收一个消息列表,把每条消息都加上相同的前缀打印出来

    • a3399a7d07847051961fd242f2048148e5dba7c1f05968d09c7843f106727caa.png

    • bdd4a98c40cf99f86a8195288a83e84b60d4749144700e1c89c538840c9c49ef.png

    • 3a2bb4f01d558edc9dfb62d6f7f01cc778b2f4ebc0c9b4d17fd4cdedceda5d16.png

    • 这里Kotlin和Java的一个显著区别就是,在Kotlin中不会仅限于访问final变量,在lambda内部也可以修改这些变量

      • 例子

        • 代码清单对给定的响应状态码set中的客户端与服务器错误分别计数
        • a06eda32d6852e2e07679039012d19c6f2eb5f4222d24525ead47422e6476e0e.png
        • ce699b25497abc92bd20e285bdfb9b92c54418c2ae51760895411980e930cd4f.png
        • 99099438beb9a1abb544747465c0f982b293053419204c779191b64cb604fc1c.png
        • 和Java不一样,Kotlin允许lambda内部访问非final变量甚至修改它们。从lambda内部访问变量,我们称这些变量被lambda捕捉
        • 就像这个例子中的prefix、clientErrors以及serverErrors一样
    • 注意:默认情况下,局部变量的生命周期被限制在声明这个变量的函数中。但是如果它被lambda捕捉了,使用这个变量的代码可以被存储并稍后再执行

    • 原理是什么呢?

      1. 当你捕捉final变量时,它的值和使用这个值的lambda代码一起存储
      2. 对非final变量来说,它的值被封装在一个特殊的包装器中,这样就可以改变这个值,而对这个包装器的引用会和lambda代码一起存储
    • 捕捉可变变量:实现细节

      • Java只允许你捕捉final变量。当想捕捉可变变量的时候,可以使用下面两种技巧:要么声明一个单元素的数组,其中存储可变值;要么创建一个包装类的实例,其中存储要改变的值的引用

      • 如果在Kotlin中显式地使用这些技术,代码看起来是这样的

        • bebc075fe20f8b3a7476756dec2683846aae1a902f0e01501c378d90b587349e.png
      • 在实际代码中,你不需要创建这样的包装器,可以直接修改这个变量:

        • 5e88d54f7688e559057c4d7b44fea27522128c9ba89f2c54a169a617de3fff95.png
      • 这是什么原理?第一个例子展示的就是第二个例子背后的原理:任何时候你捕捉了一个final变量(val),它的值被拷贝下来,这和Java一样。而当你步骤了一个可变变量(var),它的值被作为Ref类的一个实例被存储下来。Ref变量是final的能轻易地被捕捉,然而实际值被存储在其字段中,并且可以再lambda内修改

    • 注意:如果lambda被用作事件处理器或者在其他异步执行的情况,对局部变量的修改只会在lambda执行的时候发生

      • 例子

        • 下面这段代码并不是记录按钮点击次数的正确方法
        • c96349b7df4d6f1ef976db6114c6b0cc0588e432fd103e6e24679733268871fa.png
        • 这个函数始终返回0,尽管onClick处理器可以修改clicks的值,你并不能观察到值发生了变化,因为onClick处理器是在函数返回之后调用的

5、成员引用

  • 你已经看到lambda是如何把代码块作为参数传递给函数的

  • 如果想要当作参数传递的代码已经被定义成了函数,怎么办可以传递一个调用这个函数的lambda,但这样做有点多余

  • Kotlin和Java8一样,如果把函数转换成一个值,就可以传递它,使用::运算符来转换

    • 56698db369d996c920e0a71c84274ce5091586c6d418cc98dc4b9b9670e15ac5.png
  • 这种表达式称为成员引用

    • 它提供了简明语法,来创建一个调用单个方法或者访问单个属性的函数值。双冒号把类名称与要引用的成员(一个方法或者一个属性)名称隔开

      • f3a4803e7a435ae5724d04a6002aaef9eafc05d231ab90b0d791a10a396f47d8.png
  • 注意,不管你引用的是函数还是属性,都不要在成员引用的名称后面加括号

  • 成员引用和调用该函数的lambda具有一样的类型,所以可以互换使用

    • be458a7e98713804040261d3c495d8df75160b5f29ca6a291a1c5ecfbd684c2d.png
  • 还可以使用顶层函数(不是类的成员)

    • 9b5c0cb6504a156658432775b6da6d56d332e48f86a39178f922d58af6e9aa43.png
    • 这种情况下,省略了类名称,直接以::开头。成员引用::salute被当做实参传递给库函数run,它会调用相应的参数
  • 如果lambda要委托给一个接收多个参数的函数,提供成员引用代替它将会非常方便

    • 11cb0468d2c740d1b0c5e964267f8a84ec9c5e80f1def359bfd87c9a4fa1a5c5.png
  • 可以用构造方法引用存储或者延期执行创建类实例的动作。构造方法引用的形式是在双冒号后制定类名称

    • 5230b35002c6e81420a9d739536e12982bd7d495ff5761906dfbc4b3aebdbfa5.png
    • f2f6d5b8b2379f2b59496dac92736c943499907b02b0b6564c971088f97f0aae.png
  • 还可以用同样的方式引用扩展函数

    • 9afd9f542fc4d56052d83eddab36cada577088bbd48aeba3a7768b05a75a9b36.png
    • 尽管isAdult不是Person类的成员,还是可以通过引用访问它,这和访问实例的成员没什么两样
  • 绑定引用

    • 在Kotlin1.0中,当接收一个类的方法或属性引用时,始终需要提供一个该类的实例来调用这个引用
    • Kotlin1.1计划支持绑定成员引用,它允许你使用成员引用语法捕捉特定实例对象上的方法引用
    • 76377457b22f841b6f05ca6f25189a9d251af3fc09b65c0665accf7ea3e71bc6.png
    • d2c2da0f7ce607b054956c8491beaffbc8cff62ca430ef5395bed667ef8ca9e7.png
    • 注意:personsAgeFunction是一个单参数函数(返回给定人的年龄),而dmitrysAgeFunction是一个零参数的函数(返回指定好的人的年龄)。在Kotlin1.1之前,你需要显式地写出lambda{p.age},而不是使用绑定成员引用p::age

5.2、集合的函数式API

函数式编程风格在操作集合时提供了很多优势。大多数任务都可以通过库函数来完成

1、基础:filter和map

  • filter和map函数形成了集合操作的基础,很多集合操作都是借助它们来表达的

  • 每个函数我们都会给出两个例子,一个使用数字,另一个使用熟悉的Person类

  • 例子

    • filter函数遍历集合并选出应用给定lambda后会返回true的那些元素

    • 02da54fc012801532197cc2075f44216f3276d3f413f493e7e169f2ca77ee279.png

    • 92f750dd0fd7b6608b4aa6420992fdd838a9dbd2395e41b257ea64fc3425c038.png

    • 上面的结果是一个新的集合,它只包含输入集合中那些满足判断式的元素,如图

      • d4352d28fdd6b95c2139536ae6b614328ce51337cb59c3aa675311e43eed8437.png
    • 如果想留下超过30岁的人,可以使用filter

      • 5a5c0e8a738369825e5e31c5234257dcdc34c766da953a0722d5c881d4287886.png
      • a2d22284aa6fc523e608c3d1831d25386ec9eca34662fce87e86ca21ceef8d6b.png
  • filter函数可以从集合中移除你不想要的元素,但是它并不会改变这些元素元素的变换是map的用武之地

  • map函数对集合中的每一个元素对应给定的函数并把结果收集到一个新的集合

    • 例子

      • 把数字列表变换成它们平方的列表

      • 63884706384fbdb02f3d17a9f527362dcb94ce2d8a146e6923b77fdfe1083a9f.png

      • bb5ff8f8e48d337ef947caa5f8ae08d94ad666f2a9bb08768d6ee1cbbc462c1e.png

      • 结果是一个新集合,包含的元素个数不变,但是每个元素根据给定的判断式做了变换

        • 48ecffb6fae2b4540f93ddb4d3dd0603e67ac8be40d790ff00667af0fef07a90.png
      • 如果想打印只是一个姓名列表,而不是人的完整信息列表,可以用map变换列表

        • ecc7bea44103c04ee4d800f23badf0e3e92e19de6c1d3aa35b8163a986a5096c.png

        • fd506c9aca2a9b3115310f6fad29b9961086cfd24f69b939fd5e11ae2192a2eb.png

  • 可以轻松地把多次这样的调用链接起来

    • 例子

      • 打印出年龄超过30岁的人的名字
      • 59efd4749bee9feb7b2c40eed9cc4892c487be3f5ce585f7b19459c86b95ef1d.png
      • 2a63c25dda7bf45e910419447952100bb2e75587b7cc6273fde74058e0b2cd3a.png
  • 如果需要这个分组中所有年龄最大的人的名字,可以先找到分组中人的最大年龄,然后返回所有这个年龄的人,很容易就用lambda写出如下代码

    • daa50125559ff0725f32d6ee8d7d054bf0955553b7d5882f3906bd340b74d9db.png

    • 753ccb0d9dde4666a67b897b009b3f914a85aab4f4b8e1d72af4916bf6c9f366.png

    • 但是请注意,这段代码对每个人都会重复寻找最大年龄的过程,假设集合中有100个人,寻找最大年龄的过程就会执行100遍

    • 下面的解决方法做出了改进,只计算了一次最大年龄

      • d9cb10257176c42919d72f762073ad89ffece57c43acde5bd7bbbf8437a46135.png
      • 如果没有必要就不要重复计算!使用lambda表达式的代码看起来简单,有时候却掩盖了底层操作的复杂性
  • 还可以对map应用过滤和变换函数

    • 4736687f18d624a84649d3b5fcf01e7305b104f18e8e7363e971133d1ba5ef69.png
    • 8a69e34c30d42c74fd4dc6818136b2cfbba7eb77c8f90f410701240ef01c83a8.png
    • 键和值分别由各自的函数来处理
    • filterKeys和mapKeys过滤和变换map的键
    • filterValues和mapValues过滤和变换对应的值

2、“all” "any" "count"和“find”:对集合应用判断式

  • 另一种常见的任务是检查集合中所有元素是否都符合某个条件(或者它的变种,是否存在符合的元素

  • Kotlin中,它们是通过all和any函数表达的,count函数检查有多少元素满足判断式,find函数返回第一个符合条件的元素

  • 例子

    • 先定义一个判断式canBeInClub27,来检查一个人是否还没有到28岁
    • 33a9ea2516f94c48f6092c67d6994ab74002d5f06fe12dee84e6a577ea4e44e9.png
    • 06c0b04f42a85fd926c7a9d68f9d1bd73e17a6b19e856d7ab220c30a9c19885f.png
  • 注意,!all(“不是所有”)加上某个条件,可以用any加上这个条件的取反来替换,反之亦然,为了使代码更容易理解,应该选择前面不需要否定符号的函数

    • 257a9c4bc85c82c8d9da164ab202f8e85216be5c6afcbca69924ef5a22b6684e.png
  • 如果想知道有多少个元素满足了判断式,使用count

    • 439eef25fc046f9e471dcdb5c9ddb9cd96f6cc2cc75532cdc742a117bb752a05.png
    • 1d5c1eca9eed6b1dfa93a620050edd8c180d0a9ce4249677ba1cbd3fff82ef65.png
  • 使用正确的函数完成工作:“count” VS ."size"

    • count方法容易被遗忘,然后通过过滤集合之后再取大小来实现它
    • 56f06f73610ba5c5b41df891a74492010b6b99d62ec0a8a4ce9aace8bd358f62.png
    • 484dedd47cc7409a845048ea16b936355c892d575e87ecead38a6c3834c8a7c3.png
    • 在这种情况下,一个中间集合会被创建并用来存储所有满足判断式的元素
    • 在另一方面,count方法只是跟踪匹配元素的数量,不关心元素本身,所以更高效
  • 要找到一个满足判断式的元素,使用find函数

    • 091fd2c5e1179c8bf1ae0fbc7f4a01cb18051e609b2e0123141cc2aa1bef4fa4.png
    • c6456ecb04eaff303bec9f437f9db56b9e33c876a9c5357f6ff0dac28e7d686c.png
    • 如果有多个匹配的元素就返回其中第一个元素
    • 如果没有一个元素能满足判断式,返回null
    • find还有一个同义方法firstOrNull

3、groupBy:把列表转换成分组的map

  • 假设你需要把所有元素按照不同的特征划分成不同的分组

  • 例子1

    • 把人按年龄分组,相同年龄的人在一组

    • groupBy函数可以做到这一点

    • 8d1e05a480963999dbb52b201cf1d1b69a000f1c9c35f815b3a0b12edd8c7c2c.png

    • f440b394f76a7fc110a0acd7549b2f5c2331a333eefb1340c024d3db8d62002b.png

    • 这次操作的结果是一个map,是元素分组依据的键(这个例子中是age)和元素分组(persons)之间的映射

      • 36550d5abb5a9c05b3277caf1eab1b4538b8f08fb5e7eb3328805a9575ff98d8.png
    • 每一个分组都是存储在一个列表中,结果的类型就是Map<Int,List>可以使用像mapKeys和mapValues这样的函数对这个map做进一步的修改

  • 例子2

    • 使用成员引用把字符串按照首字母分组

    • 642b1ea2021f3d0f419be39a8cb4ef3d16ad1f4e66aeb6eae8b7237142d16e49.png

    • 2331b7b75629ad37ba1737c02c918a0b49f36de8c5e6c522d8f521f8fa98d0b3.png

    • 注意:这里first并不是String类的成员,而是一个扩展

4、flatMap和flatten:处理嵌套集合中的元素

  • 假设你有一堆仓鼠,使用Book类表示

  • 0d58fbf90baf29d18f7569c4ef97a544fbe83cfda7cf0db8ceabf0cc123303f4.png

  • 每本书都可能有一个或者多个作者,可以统计出图书馆中所有作者的set

  • bff08e34b67329098ef93620bbc7eaba3ad9468b7329ecb40f36effc301880b2.png

  • flatMap函数做了两件事情:首先根据作为实参给定的函数对集合中的每个元素做变换(或者说映射),然后把多个列表合并(或者说平铺)成一个列表

  • 例子

    • 43f5f2f6e810c8b7369a2df924693ba3cf9190b95337669cc891614d879f73b7.png
    • 1f95b2c70eecba96483989d1b810df794df0b351949a91a08ea56fec1e3bc99f.png
    • 109ed2dad0cd993337844da69613122a5a0c40e16e079dcf3edb5c0951dca49d.png
    • 字符串上的toList函数把它转换成字符列表。如果和toList一起使用的是map函数,会得到一个字符列表的列表,就如图中的第二行。flatMap函数还会执行后面的步骤,并返回一个包含所有元素(字符)的列表
  • 让我们回到书籍作者的例子

    • dc2c3f7c05d639693c84cbb526c64797c7c8f09461b78688a7e458c3fbf11a38.png
    • fbad81169359f0ac277f710cc1bf3b47d5d75d6fb8f8a8ff8b79855b8254f809.png
    • 每一本书都可能有多位作者,属性book.authors存储了每本书的作者集合
    • flatMap函数把所有书籍的作者合并成了一个扁平的列表
    • toSet调用移除了结果集合中的所有重复元素
    • 当你卡壳在元素集合的集合不得不合并成一个的时候,你可能会想起flatmap
    • 如果不需要任何变换,只是需要平铺一个集合,可以使用flatten函数:listOfLists.flatten()

5.3、惰性集合操作:序列

map和filter函数会及早地创建中间集合,也就是说每一步的中间结果都被存储在一个临时列表。序列给了你执行这些操作的另一种选择,可以避免创建这些临时中间对象

看例子

  • 35dd5519b9478206559a25409541a19666e2047d7e036543f4091e61bca0c140.png

  • 594e79e3f2a3ca9acdfcd2b7943faf67d07cb5c43745abc2f5e69e6ae37b34b8.png

  • Kotlin标准库参考文档有说明,filter和map都返回一个列表

  • 这意味着上面例子中的链式调用会创建两个列表:一个保存filter函数的结果,两一个保存map函数的结果

  • 如果源列表只有两个元素,这不是什么问题,但是如果有一百万个元素,(链式)调用就会变得十分低效

  • 为了提高效率,可以把操作编程使用序列,而不是直接使用集合:

    • de9b9d80e2c0984e5141fb9e65d370e75fc75177170ffac2749268ee3fbf2d2b.png
    • 33ec1dd0acb1d84ad9b5f61ab1a8d12ab5a50058f398bc3680916e4cf7b0fcdf.png
    • 该例子没有创建任何用于存储元素的中间集合,所以元素数量巨大的情况下性能将显著提升
  • Kotlin惰性集合操作的入口就是Sequence接口这个接口表示的就是一个可以逐个列举元素的元素序列。Sequence只提供了一个方法,iterator,用来从序列中获取值

  • Sequence接口的强大之处在于其操作的实现方式。序列中的元素求值是惰性的。因此,可以使用序列更高效地对集合元素执行链式操作,而不需要创建额外的集合来保存过程中产生的中间结果

  • 可以调用扩展函数asSequence把任意集合转换成序列,调用toList来做反向的转换

  • 为什么需要把序列转换回集合?用序列代替集合不是更方便吗?特别是它们还有这么多优点。答案是:有的时候是这样。如果你只需要迭代序列中的元素,可以直接使用序列。如果你要使用其他API方法,比如用下标访问元素,那么需要把序列转换成列表。

  • 注意

    • 通常,需要对一个大型集合执行链式操作时要使用序列

1、执行序列操作:中间和末端操作

  • 序列操作分为两类:中间的和末端的

  • 一次中间操作返回的是另一个序列,这个新序列知道如何变幻原始序列中的元素

  • 一次末端操作返回的是一个结果,这个结果可能是集合、元素、数字,或者其他从初始集合的变幻序列中获取的任意对象

    • 74950ebc7b343fa6588c3dd5da0fcb1bc7011b6b8dfd541f30b724587ddc3b8e.png
  • 中间操作始终都是惰性的

    • 例子

      • 9682276345f9df424854d99fb736f3171a4d46c1f0271600d95c0fc012c11454.png

      • 执行这段代码并不会在控制台上输出任何内容,这意味着map和filter变换被延期了,它们只有在获取结果的时候才会被应用(即末端操作被调用的时候)

      • 60c1b3eaf0951a5bd73cdc5f037c161e47f4af940656f555c8dfb84b28517566.png

      • af8905837d751688e7907d5165bec380a36af9406959aff0d50a981dd7e6a060.png

      • 末端操作触发执行了所有的延期计算

      • 这个例子中另外一件值得注意的重要事情是计算执行的顺序。一个笨方法是先在每个元素上调用map函数,然后在结果序列的每个元素上再调用filter函数

      • map和filter对集合就是这样做的,而序列不一样

      • 对序列来说,所有操作是按顺序应用在每一个元素上:处理完第一个元素(先映射再过滤),然后完成第二个元素的处理,以此类推

      • 这种方法意味着部分元素根本不会发生任何变换,如果在轮到它们之前就已经取得了结果

        • 例子

          • 首先把一个数字映射成它的平方,然后找到第一个比数字3大的条目

          • 380c71e85e9194c8d38c6a5246033071f83fffcc835ad0aac07779d55673cd83.png

          • eb201951ec411eb83d9d2c9629b45e7ee3771514cccf1f9fb09c81197fbac462.png

          • 如果同样的操作被应用在集合而不是序列上时,那么map的结果首先被求出来,即变换初始集合中的所有元素。第二步,中间集合中满足判断式的一个元素会被找出来

          • 而对于序列来说,惰性方法意味着你可以跳过处理部分元素,图右阐明了这段代码两种求值之间的区别,一种是及早求值(使用集合),一种是惰性求值(使用序列)

            • 4d64cd4e8af32affe2cf09a7ef3aed1ec30ea6adf96b0f81a63b0c5cce4e0dee.png
            • 第一种情况,当你使用集合的时候,列表被变换成了另一个列表,所以map变换应用到每一个元素上,包括了数字3和4,然后第一个满足判断式的元素被找到了:数字2的平方
            • 第二种情况,find调用一开始就逐个地处理元素。从原始序列中取一个数字,用map变换它,然后再检查它是否满足传给find的判断式,当进行到数字2时,发现它的平方已经比数字3大,就把它作为find操作结果返回了,不再需要继续检查数字3和4,因为这之前已经找到了结果
          • 在集合上执行操作的顺序也会影响性能。假设有一个人的集合,想要打印集合中那些长度小于某个限制的人名,需要做两件事:把每个人映射成它们的名字,然后过滤掉其中不够短的名字。这种情况下可以用任何顺序应用map和filter操作,两种顺序得到的结果一样,但他们应该执行的变换总次数是不一样的

            • 6a8df5646333822573f915dd8f485636af9f2b598700baa68b819ccdb415035d.png
            • e6019ac610ddc1aa254ebc1dda0141842adff683ca5cb0e075159e4f006e3b7c.png
            • a25c95e17d004aaf217dfb1634926c42e8a6b74af5c75ee5d20aaaec01276471.png
            • 如果map在前,每个元素都被变换,而如果filter在前,不合适的元素会被尽早地过滤掉且不会发生变换

2、创建序列

  • 前面的例子都是使用同一个方法创建序列:在集合上调用asSequence()

  • 另一个可能性是使用generateSequence函数,给定序列中的前一个元素,这个函数会计算出下一个元素

  • 例子

    • 计算100以内所有自然数之和
    • b06f5f0d4022dc8189ced2b488c917aa1267e7fba6e61f42aa9866fe3cc4905b.png
    • fd89505a1b6f2b1c7093abbeabf826301c3006dbfbe33babe1ebc3ab1fecbf43.png
    • 注意,这个例子中的naturalNumbers和numbersTo100都是有延期操作的序列。这些序列中的实际数字直到你调用末端操作(这里是sum)的时候才会求值
  • 另一个常见的用例是父序列。如果元素的父元素和它的类型相同(比如人类或者Java文件),你可能会对它所有祖先组成的序列的特质特别感兴趣

    • 例子

      • 查询文件是否放在隐藏目录中,通过创建一个其父目录的序列并检查每个目录的属性来实现
      • a6ca41564eae099b1aece0a64cf85b22272a8354d2017fd8a5946c23ed584846.png
      • a5c86a78c44d01de8e83d5416b257b9b621ef91009eea5f237fba6c8177afb81.png
      • 6cffc36c9624594339523bc2479c91978c5c7497a7429e164b546a052c90e613.png
      • 生成了一个序列,通过提供第一个元素和获取每个后续元素的方式来实现
      • 如果把any换成find,还可以得到想要的那个目录(对象)。注意,使用序列允许你找到需要的目录之后立即停止遍历父目录

5.4、使用Java函数式接口

和Kotlin库一起使用lambda感觉不错,但是大部分API很可能是用Java而不是Kotlin写的,Kotlin的lambda可以无缝地和Java ApI互操作

例子

  • lambda传给Java方法的例子

  • b9cd007ed0fb67ca018e24d8ca127c2e16cf968d4f0a9208d73fcd7968ad00c5.png

  • Button类通过接受类型为OnClickListener的实参的setOnClickListener方法给按钮设置一个新的监听器

    • 988189e6ff1ee2c59afe0007d3d71b8a4ab0cb83b7abaca7629c05ca0b81ff90.png
  • OnClickListener接口只声明了一个方法,onClick

    • 91b02e30f74bc9ccf16453e19bf878fe3a6653e5e6a621e9827d301716246dd7.png
  • 在java8之前,不得不创建一个匿名类的实例来作为实参传递给setOnClickListener方法

    • f7ba2c15247598958df865587f8897bcc19ccf76d54e8a74a7fdfa0741e5e300.png
  • 在Kotlin中,可以传递一个lambda,代替这个实例

    • b31cda21227755478d45691367f2742db242d50c011549adc5e26d84200cb8c8.png
  • 这个lambda用来实现OnClickListener,它有一个类型为View的参数,和OnClick方法一样,图展示了这个映射

    • 293f301b9ba75426de203731850a5e1608ad9185fc4f64526c473aaa80a5b8cc.png
  • 函数式接口

    • 这种方式可以工作的原因是OnClickListener接口只有一个抽象方法。这种接口被称为函数式接口,或者SAM接口,SAM代表单抽象方法
    • 接口只有一个抽象方法,该接口被称为函数式接口
  • Java API中随处可见像Runnable和Callable这样的函数式接口,以及支持它们的方法

  • 注意

    • 和Java不同,Kotlin拥有完全的函数类型,正因为这样,需要接收lambda作为参数的Kotlin函数应该使用函数类型而不是函数式接口类型,作为这些参数的类型
    • Kotlin不支持把lambda自动转换成实现Kotlin接口的对象

1、把lambda当作参数传递给Java方法

  • 可以把lambda传给任何期望函数式接口的方法

  • 例子

    • 有一个Runnable类型的参数

    • b79c5aaad81231c7698e39bc246f923e0e9f7908415f31f8a2a143da7d9cf64d.png

    • 在Kotlin中,可以调用它并把一个lambda作为实参传给它,编译器会自动把它转换成一个Runnable的实例

    • 5e6f6ce41e492397badf72269e585a33a339c93068ac7fb062799efeeb0bc9b4.png

    • 注意,当我们说“一个Runnable的实例”时,指的是“一个实现了Runnable接口的匿名类的实例”。编译器会帮你创建它,并使用lambda作为单抽象方法--这个例子中是run方法--的方法体

    • 通过显式地创建一个实现了Runnable的匿名对象也能达到同样的效果

    • 40760ecd15806a6df2ae3c0911a9276daa9a37f051c180390a448fe84bf3ad2c.png

    • 当你显示地声明对象时,每次调用都会创建一个新的实例

    • 使用lambda的情况不同:如果lambda没有访问任何来定义它的函数的变量,相应的匿名类实例可以在多次调用之间重用

    • 85c1897f3f2c15b0b8b37884bc0fd2ec70fb85c453932566ed671088bd2b4fef.png

    • 完全等价的实现应该是下面这段代码中的显式object声明,它把Runnable实例存储在一个变量中,并且每次调用的时候都使用这个变量

    • 4157c08cbfecf0775e45c0ca70daccb8ba43d85e0890e7f08adad034540f00a8.png

    • 如果lambda从包围它的作用域中捕捉了变量,每次调用就不再可能重用同一个实例了,这种情况下,每次调用时编译器都要创建一个新对象,其中存储着被捕捉的变量的值

      • 下面这个函数,每次调用都会使用一个新的Runnable实例,把id值存储在它的字段中
      • da26787c6a06f7f4af755342fe0ad40207eda861736a64231b33002702cc4cdc.png
  • Lambda的实现细节

    • 自Kotlin1.0起,每个lambda表达式都会被编译成一个匿名类,除非它是一个内联lambda

    • 后续版本计划支持生成Java8字节码。一旦实现,编译器就可以避免为每一个lambda表达式都生成一个独立的.class文件

    • 如果lambda捕捉了变量,每个被捕捉的变量会在匿名类中有对应的字段,而且每次(对lambda的)调用都会创建一个这个匿名类的新实例。否则,一个单例就会被创建

    • 类的名称由lambda声明所在的函数名称加上后缀衍生出来:这个例子中就是HandleComputation$1。如果你反编译之前lambda表达式的代码就会看到:

      • 42d6fb8ba4c5374d90227c0d3915107451edfa7485e3f292c3f48ccf96ffec3d.png
    • 如你所见,编译器给每个被捕捉的变量生成了一个字段和一个构造方法参数

    • 请注意这里讨论的为lambda创建一个匿名类,以及该类的实例的方式只对期望函数式接口的Java方法有效,但是对集合使用Kotlin扩展方式的方式并不适用

    • 如果你把lambda传给了标记成inline的Kotlin函数,是不会创建任何匿名类的。而大多数库函数都标记成了心里呢,这里的细节讲在8.2节讨论

    • 如你所见,大多数情况下,从lambda到函数式接口的实例的转换都是自动发生的,不需要你做什么。但是也存在需要显式地执行转换的情况

2、SAM构造方法:显式地把lambda转换成函数式接口

  • SAM构造方法是编译器生成的函数,让你执行从lambda到函数式接口实例的显式转换

  • 可以在编译器不会自动应用变换的上下文中使用它

  • 例子

    • 如果有一个方法返回的是一个函数式接口的实例,不能直接返回一个lambda,要用SAM构造方法把它包装起来
    • 38de996ad496cc1e02ac1c8219bd8c431a9a4ab3a65e4807aa7d516dad98b656.png
    • 5db9aede91c89b461cddb01f1a5ba6cecfc2589aa8bc36ee585d4c84b257773b.png
  • SAM构造方法的名称和底层函数式接口的名称一样。SAM构造方法只接收一个参数-一个被用作函数式接口单抽象方法体的lambda-并返回实现了这个接口的类的一个实例

  • 除了返回值外,SAM构造方法还可以用在需要把从lambda生成的函数式接口实例存储在一个变量中的情况

  • 例子

    • 假设你要在多个按钮上重用同一个监听器,就像下面的代码清单一样(在Android应用中,这段代码可以作为Activity.onCreate方法的一部分)
    • 3af0c6955f66dab25ba9bccfb68127322c72c793424b960cff3fc19529ed0821.png
    • listener会检查哪个按钮是点击的事件源并做出相应的行为。可以使用实现了OnClickListener的对象声明来定义监听器,但是SAM构造方法给你更简洁的选择
  • Lambda和添加/移除监听器

    • 注意lambda内部没有匿名对象那样的this:没有办法引用到lambda转换成的匿名类实例
    • 从编译器的角度来看,lambda是一个代码块,不是一个对象,而且也不能把它当成对象引用
    • Lambda中的this引用指向的是包围它的类
    • 如果你的事件监听器在处理事件时还需要取消它自己,不能使用lambda这样做,这种情况使用实现了接口的匿名对象,在匿名对象内,this关键字指向该对象实例,可以把它传给移除监听器的API
  • 尽管方法调用中的SAM转换一般都自动发生,但是当把lambda作为参数传递给一个重载方法时,也有编译器不能正确的重载的情况。这时,使用显示的SAM构造方法是解决编译器错误的好方法

5.5、带接收者的lambda:"with"与“apply”

Kotlin的lambda的独特功能:在lambda函数体内可以调用一个不同对象的方法,而且无需借助任何额外的限定符:这种能力在Java中是找不到的。这样的lambda叫作带接收者的lambda

1、“with”函数

  • 很多语言都有这样的语句,可以用它对同一个对象执行多次操作,而不需要反复把对象的名称写出来

  • Kotlin提供一个叫with的库函数,而不是某种特殊的语言结构

  • 例子

    • 747e3f6717001e8477bf99124e7b26885848c51d75fe67fd9ad2a2385dd4fce3.png

    • a39e1fd211026ed2f04e0a7f5f377d0d3676897528e613b1e6fe83ea80242bbe.png

    • 75f914970dbadc8c08c4ca734cbda654c5305f666d88017f91665977fbef525c.png

    • 上面例子中,调用了result实例上好几个不同的方法,而且每次调用都要重复result这个名称

    • 这里的情况还不算太糟,但是如果用的表达式更长或者重复的更多,该怎么办?

    • 使用with构建字母表

      • 1e5e58106ee27a59d501559e47f6cfdb485f1b91c4d97412c7b7f6cb4ccbd79f.png
      • 68cfefa57bf86247cd53fbf1206a5d5757c2f44f7af8210bfce58bd6c0540be2.png
      • 75f914970dbadc8c08c4ca734cbda654c5305f666d88017f91665977fbef525c.png
      • with结构看起来像是一种特殊的语法结构,但它实际上是一个接收两个参数的函数:这个例子中两个参数分别是stringBuilder和一个lambda
      • 这里利用了把lambda放在括号外的约定,这样整个调用看起来就像是内建的语言功能
      • with函数把它的第一个参数转换成作为第二个参数传给它的lambda的接收者
      • 可以显式地通过this引用来访问这个接收者,或者按照惯例,可以省略this引用,不用任何限定符直接访问这个值的方法和属性
      • 例子中,this指向了stringBuilder,这是传给with的第一个参数,可以通过显式的this引用来访问stringBuilder的方法,就像this.append(letter)这样:也可以像append("\nNow...")这样直接访问
    • 带接收者的lambda和扩展函数

      • this指向的是函数接收者

      • 在扩展函数内部,this指向了这个函数扩展的那个类型的实例,而且也可以被省略掉,可以直接访问接收者的成员

      • 注意,一个扩展函数某种意义上来说就是带接收者的函数,可以做下面的对比

        • 0c91db4969a5fe192f732cebbbd7b140ac67c968b897d3cd46777bf8acf99d01.png
      • Lambda是一个类似普通函数的定义行为的方法,而带接收者的lambda是类似扩展函数的定义行为的方式

    • 重构初始化的alphabet函数,去掉额外的stringBuilder变量

      • cfb0fbad8ded94f8a6775b762b8c481aae2d3650f7a3be88cb1444672b9d589d.png
      • d24301c646f735cbaa841ad27129f6cc40f625199eaf36d5d9882bd6dc774dc8.png
      • 5ec4b2af533bf30bd75ed916160517c13391fd02322eb086b462e53371e0d5b7.png
      • 这个函数只返回一个表达式,所以使用表达式函数体语法重写了它
      • 可以创建一个新的StringBuilder实例直接当作实参传给这个函数,然后再lambda中不需要显式的this就可以引用到这个实例
    • 方法名称冲突

      • 如果你当作参数传递给with的对象已经有这样的方法,该方法的名称和你正在使用with的类中的方法一样,怎么办?这种情况下,可以给this引用加上显式的标签来表明你要调用的是哪个方法

      • 假设函数alphabet是类OuterClass的一个方法,如果你想引用的是定义在外部类的toString方法而不是StringBuilder,可以用下面这种语法

        • ade678364bc18590ae515386350874c9307e97dda4adc76974bccb80de36701c.png
    • with返回的值是执行lambda代码的结果,该结果就是lambda中的最后一个表达式(的值)

    • 但是有时候你想返回的是接收者对象,而不是执行lambda的结果,这时apply库函数就派上用场了

2、“apply”函数

  • apply函数几乎和with函数一模一样,唯一的区别是apply始终会返回作为实参传递给它的对象(换句话说,接收者对象)

  • 例子

    • edcb2733517ef88b1aca3440bfa2bf622c1231629f6115dee05cc5716bd38d26.png
    • e57922db21f44b2ea9438b3c6f736bc5a0324e5f119f629f73739de490b0c423.png
    • ff6cdc6fbaebd74ca2feccfadb9c00755fbed831e9bac33025e420711f8e2221.png
    • apply被声明成一个扩展函数。它的接收者编程了作为实参的lambda的接收者
    • 执行apply的结果是StringBuilder,所以接下来可以调用toString
  • 许多情况下apply都很有效,其中一种是创建一个对象实例并需要用正确的方式初始化它的一些属性的时候

  • 在Java中,这通常是通过另外一个单独的Builder对象来完成的:而在Kotlin中,可以在任意对象上使用apply,完全不需要任何来自自定义该对象的库的特别支持

  • 例子

    • 49a7a88f4367f74b313fae4ef92a69399094f6c76a3cca7a548ff11a4ad30545.png
    • apply函数允许你使用紧凑的表达式函数体风格
    • 新的TextView实例创建之后立即被传给了apply。在穿个apply的lambda中,TextView实例变成了(lambda)接收者,你就可以调用它的方法并设置它的属性
    • Lambda执行之后,apply返回已经初始化过的接收者实例,它编程了createViewWithCustomAttributes函数的结果
  • with函数和apply函数是最基本和最常用的使用带接收者的lambda的例子。更多具体的函数也可以使用这种模式

  • 例如你可以使用标准库函数buildString进一步简化alphabet函数,它会负责创建StringBuilder并调用欧冠toString。buildingString的实参是一个接收者的lambda,接收者就是StringBuilder

    • ad7ebdce400e087fc8d4392ac7a7f60320d9cac16cac889a5cdcf9021e476d13.png
    • 8ab6dfd991395920807f035aac74ed597c016568def1d1b6833939b40e50f2dc.png

5.6、小结

Lambda允许你把代码块作为参数传递给函数

Kotlin可以把lambda放在括号外传递给函数,而且可以用it引用单个的lambda参数

lambda中的代码可以访问和修改包含这个lambda调用的函数的变量

通过在函数名称前加上前缀::,可以创建方法、构造方法及属性的引用,并用这些引用代替lambda传递给函数

使用像filter、map、all、any等函数,大多数公共的集合操作不需要手动迭代元素就可以完成

序列允许你合并一个集合上的多次操作,而不需要创建新的集合来保存中间结果

可以把lambda作为实参传递给接受Java函数式接口(带单抽象方法的接口,也叫做SAM接口)作为形参的方法

带接收者的lambda是一种特殊的lambda,可以在这种lambda中直接访问一个特殊接收者对象的方法

with标准库函数孕育你调用同一个对象的多个方法,而不需要反复写出这个对象的引用。apply函数让你使用构建者风格的API创建和初始化任何对象

附件:

第5章Lambda编程.svg

Kotlin学习之旅开始啦

第1章Kotlin:定义与目的

第2章Kotlin基础

第3章函数的定义和调用

第4章类、对象和接口