Lambda 编程

Lambda 表达式和成员引用

Lambda 表达式,简称 Lambda ,本质上就是可以传递给其他函数的一小段代码。

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

我们来看一个例子,假设你要定义一个按钮的点击行为,添加一个负责处理点击的监听器,监听器实现了相应的接口 OnClickListener 和它的一个方法 onClick

/* Java */
button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //do something
        }
});

这样声明匿名内部类的写法实在是太啰嗦了。在 Kotlin 中我们可以像 Java 8 一样使用 lambda 来消除这些冗余代码。

/* Kotlin */
button.setOnClickListener{ /* do someting */ }

Lambda 和集合

我们来看一个例子,你会用到一个 Person 类,它包含了这个人的名字和年龄信息。

data class Person(val name: String, val age: Int)

假设你现在有一个 Person 类列表,需要找到列表中年龄最大的那个人,如果完全不了解 lambda ,你可能会这样做:

fun findTheOldest(list: List<Person>) {
    var maxAge = 0
    var theOldestPerson: Person? = null     // ? 表示可以为null

    for (p in list) {
        if (p.age > maxAge) {
            maxAge = p.age
            theOldestPerson = p
        }
    }
    println(theOldestPerson)
}

val personList = listOf(Person("fanda", 18), Person("liuhang", 13))
findTheOldest(personList)

这样做可以得到结果,但是代码繁多且很容易出错。而在 Kotlin 中可以使用库函数,如下所示:

val personList = listOf(Person("fanda", 18), Person("liuhang", 13))
personList.maxBy { it.age }

maxBy 函数可以在任何集合上调用,且只需要一个实参:一个函数,指定比较哪个值来找到最大的元素。 {it.age} 就是实现了这个逻辑的 lambda 。它接收一个集合中的元素作为实参(使用 it 引用它)并且返回用来比较的值。这个例子中,集合元素是 Person 对象,用来比较的是存储在其 age 属性中的年龄。

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

// 注意,这里用的是 () ,括号里面是成员引用,成员引用等价于 lambda
personList.maxBy(Person::age)

其实,上述用 lambda 的方式可以是这样的:

// 把去掉的括号显示出来
personList.maxBy ({ it.age })

虽然 lambda 看上去很简洁,但是你可能不是很明白到底如何写 lambda ,以及里面的规则,我们来学习下 lambda 表达式的语法吧。

Lambda 表达式的语法

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

                  // 参数      //分隔符  // 函数体
{  x: Int, y: Int    ->       x + y }

Kotlinlambda 表达式始终用花括号包围,实参并没有用括号括起来, -> 把实参列表和函数体隔开,当 lambda 被独立地声明并存储到一个变量中时,可以把这个变量当作普通函数对待(即通过相应实参调用它)。

val lambda = {  x: Int, y: Int -> x + y }    // 存在 lambda 变量中
// 像调用方法一样调用 lambda 表达式
println(lambda(13, 15))

上述找到年龄最大的人的例子中,使用的是最简的 lambda 表达式,如果不用任何简明语法来重写那个例子,会是下面这样:

personList.maxBy({ p: Person -> p.age })

这段代码一目了然,括号就是函数调用的语法,花括号就是 lambda 表达式,该表达式作为参数传入 maxBy 函数,该表达式接收一个 Person 的参数并返回它的年龄。

但是语法太过啰嗦,可以进行改进,首先是花括号: Kotlin 有这样一种语法约定,如果 lambda 表达式是函数调用的最后一个实参,它可以放到括号的外边,上述 maxBy 函数只有一个参数,明显也就是最后一个实参了,那么可以这样写:

personList.maxBy() { p: Person -> p.age }

lambda 是函数唯一的实参时,还可以去掉调用代码中的空括号:

// 去掉函数调用的括号
personList.maxBy { p: Person -> p.age }

类型可以通过上下文推断出来并可以省略:

personList.maxBy { p -> p.age }        //通过类型推导,省略类型

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

如果当前上下文期望的是只有一个参数的 lambda 且这个参数的类型可以推断出来,就会生成默认参数名称 it ,这样就不用显式进行参数命名了,即 p 这个命名可以省略了,由于没有了参数,那么 -> 也可以省略掉:

personList.maxBy { it.age }    // it 是自动生成的默认参数名称

我们总结一下,以下所有的表达式都是等价的:

personList.maxBy({ p: Person -> p.age })
personList.maxBy() { p: Person -> p.age }
personList.maxBy { p: Person -> p.age }
personList.maxBy { p -> p.age }
personList.maxBy {  it.age }

注意:只有当实参名称没有显式指定时,这个默认的名称才会生成,如果指定了,用指定的命名即可。如果上下文中参数的类型或意义不是很明朗或在嵌套 lambda 的情况下,最好显式声明每个 lambda 的参数。当然,如果你用变量存储 lambda ,那么肯定没有可以推断参数类型的上下文,必须显式指定参数类型。

至此,你看到的例子都是单个表达式或语句构成的 lambda ,但是 lambda 并没有被限制在这样小的规模,它可以包含更多的语句。下面这种情况,最后一个表达式就是 (lambda) 的结果:

val sum = {x: Int,y: Int ->
    println("Computing the sum of $x and $y")
    x + y
}

println(sum(43,1))
// 输出
Computing the sum of 43 and 1
44

在作用域中访问变量

当在函数内声明一个匿名内部类的时候,能够在这个匿名内部类引用这个函数的参数和局部变量。也可以用 lambda 作同样的事情。如果在函数内部使用 lambda ,也可以访问这个函数的参数,还有在 lambda 之前定义的局部变量

我们用标准库函数 forEach 来展示这种行为。这个函数能够遍历集合中的每一个元素,并在该元素上调用给定的 lambdaforEach 函数只是比普通 for 循环更简洁一些,并没有其他性能上的优势。

fun printMessageWithPrefix(messages: Collection<String>, prefix: String) {
    // 接收 lambda 作为实参指定对每个元素的操作
    messages.forEach {
        // 在 lambda 中访问 "prefix" 参数
        println("$prefix $it")
    }
}

val errors = listOf("403 Forbidden", "404 Not Found")
printMessageWithPrefix(errors, "Error:")

// 输出
Error: 403 Forbidden
Error: 404 Not Found

Java 不一样,Kotlin 允许在 lambda 内部访问非 final 变量甚至修改它们,我们称这些变量被 lambda 捕获,示例如下:

fun printProblemCounts(response: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0

    response.forEach {
        if (it.startsWith("4")) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}

val errors = listOf("403 Forbidden", "404 Not Found")
printProblemCounts(errors)

它的原理跟我们之前讲的匿名内部类访问非 final 变量甚至修改它们的原理是一样的,当你捕获 final 变量时,它的值和使用这个值的 lambda 代码一起存储。而对非 final 变量来说,它的值被封装在一个特殊的包装器类中,这样你就可以改变这个值,而对这个包装器的引用会和 lambda 代码一起存储。

成员引用

我们已经看到 lambda 是如何让你把代码块作为参数传递给函数的人,但当你想当作参数传递的代码已经被定义成了函数,该怎么办? KotlinJava 8 一样,如果把函数转换成一个值,你就可以传递它,使用 :: 运算符来转换:

             // 类    //成员
val getAge = Person::age

这种表达式称为成员引用,它提供了简明语法,来创建一个调用单个方法或访问单个属性的函数值。双冒号把类名称与你要引用的成员(一个方法或一个属性)名称隔开。同样的内容用 lambda 表达式实现是这样的:

val getAge = { p :Person -> p.age}

注意: 不管引用的是函数还是属性,都不要在成员引用的名称后面加括号。成员引用和调用该函数的 lambda 具有一样的类型,可以互换使用。

还可以引用顶层函数(不是类的成员,直接写在文件的函数):

// 这是一个顶层函数
fun salute() = println("salute")
// 引用顶层函数作为实参传递给 run 函数
run(::salute)    // 省略了类名

如果 lambda 要委托给一个接收多个参数的函数,提供成员引用代替它将会非常方便:

fun sendEmail(person: Person, message: String) {
}

// 有两个参数的 lambda
val action = {person: Person,message :String ->
    // 委托给 sendEmail 函数
    sendEmail(person,message)
}

// 利用成员引用的方式,等价于前一个 action 
val nextAction = ::sendEmail

可以用构造方法引用存储或延期执行创建类实例的作用,构造方法引用的形式是在双冒号后指定类名称:

fun printPerson() {
    // 创建 Person 实例的动作被保存成了值
    val createPerson = ::Person
    // 延期执行创建类实例
    val p = createPerson("fanda", 19)
    println(p)
}

还可以用同样的方式引用扩展函数:

// 扩展函数
fun Person.isAdult() = age >= 18
// 成员引用扩展函数
val predicate = Person::isAdult
predicate(person)

尽管 isAdult 不是 Person 类的成员,还是可以通过成员引用访问它,这和访问实例的成员没什么两样: person.isAdult()

注意:无论是成员引用的属性还是函数,生成的都是一个函数值,使用的时候要用括号。

绑定引用

Kotlin 1.0 中,当接受一个类的方法或属性引用时,你始终需要提供一个该类的实例来调用这个引用。 Kotlin 1.1 计划支持绑定成员引用,它允许你使用成员引用语法捕获特定实例对象上的方法引用。

// 创建 Person 实例的动作被保存成了值
val createPerson = ::Person
// 延期执行创建类实例
val p = createPerson("fanda", 19)

val predicate = Person::isAdult
// 提供一个该类的实例来调用,需要传入参数
println(predicate(p))

// 捕获特定实例对象上的方法引用
val predicate = p::isAdult
// 不用传入参数
println(predicate())

集合的函数式 API

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

基础: filter 和 map

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

val list = listOf(1, 2, 3, 4)
//输出 [2, 4]
println(list.filter { it % 2 == 0 })

返回的结果是一个新的集合,它只包含集合中那些满足判断的元素。如果你想留下那些超过 30 岁的人,可以用 filter 这样写:

val personList = listOf(Person("fanda", 38), Person("liuhang", 22))
// 输出  [Person(name=fanda, age=38)]
println(personList.filter { it.age > 30 })

map 函数对集合中的每个元素应用给定的函数并把结果收集到一个新集合,可以把数字列表变换成它们平方的列表,比如:

val list = listOf(1, 2, 3, 4)
// 输出 [1, 4, 9, 16]
println(list.map { it * it })

结果是一个新的集合,包含的元素的个数不变,但是每个元素根据给定的判断式做了变换处理。如果你想打印的只是一个姓名列表,而不是人的完整信息,可以用 map 来变换列表:

val personList = listOf(Person("fanda", 38), Person("liuhang", 22))
// 输出  [fanda, liuhang]
println(personList.map { it.name })
// 成员引用的写法
println(personList.map(Person::name))

可以轻松地把多次这样的调用链接起来,例如,打印出年龄超过 30 岁的人的名字:

// [fanda]
println(personList.filter { it.age > 30 }.map { it.name })

现在,如果说需要这个分组中所有年龄最大的人的名字,可以先找到分组中人的最大年龄,然后返回所有这个年龄的人,很容易就用 lambda 写出如下代码:

// !! 是让编译器不要判断元素是否为 null
println(personList.filter { it.age == personList.maxBy(Person::age)!!.age })

但是注意,这段代码对每个人都会重复寻找最大年龄的过程,假设集合中有 100 个人,寻找最大年龄的过程就会执行 100 遍!下面的解决方法做出了改进,只计算了一次最大年龄:

val maxAge = personList.maxBy(Person::age)!!.age
println(personList.filter { it.age == maxAge})

如果没有必要就不要重复计算!使用 lambda 表达式的代码看起来简单,有时候却掩盖底层操作的复杂性。始终牢记你写的代码在干什么。

还可以对 map 集合应用过滤和变换函数:

val numbers = mapOf(0 to "zero", 1 to "one")
// 变换值  {0=ZERO, 1=ONE}
println(numbers.mapValues { it.value.toUpperCase() })
// 过滤值  {0=zero}
println(numbers.filterValues { it.length > 3 })

// 变换键  {1=zero, 2=one}
println(numbers.mapKeys { it.key + 1 })

// 过滤键  {1=one}
println(numbers.filterKeys { it > 0 })

键和值分别由各自的函数来处理。 filterKeysmapKeys 过滤和变换 map 集合的键,而另外的 filterValuesmapValues 过滤和变换对应的值。

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

另一种常见的任务是检查集合中所有元素是否都符合某个条件(或者它的变种,是否存在符合的元素)。 Kotlin 中,它们是通过 allany 函数表达的。 count 函数检查有多少元素满足判断式,而 find 函数返回第一个符合条件的元素。
为了演示这些函数,我们先来定义一个判断式,来检查一个人是否还没有到 28 岁:

val canBeInClub27 = {p: Person -> p.age <= 27}

如果你对是否所有元素都满足判断式感兴趣,可以用 all 函数:

val personList = listOf(Person("fanda", 38), Person("liuhang", 22))
println(personList.all(canBeInClub27))  // false

如果你要检查集合中是否至少存在一个匹配的元素,那就用 any 函数:

val personList = listOf(Person("fanda", 38), Person("liuhang", 22))
println(personList.any(canBeInClub27))  // true

注意: !all(不是所有)加上某个条件,可以用any加上这个条件的取反来替换,反之亦然。为了让你的代码更容易理解,应该选择前面不需要否定符号的函数。

val personList = listOf(Person("fanda", 38), Person("liuhang", 22))
// true
println(!personList.all { it.age == 22 })  // ! 否定不明显,最好用 any 替换
// true
println(personList.any { it.age != 22 })  // lambda 参数中的条件要取反

如果你想知道有多少个元素满足了判断式,就使用 count 函数:

val personList = listOf(Person("fanda", 38), Person("liuhang", 22))
println(personList.count(canBeInClub27))  // 1

count 函数很容易被遗忘,然后通过过滤集合之后再取大小来实现:

val personList = listOf(Person("fanda", 38), Person("liuhang", 22))
println(personList.filter(canBeInClub27).size)  // 1

在这种情况下,一个中间集合会被创建并用来存储所有满足判断式的元素。而另一方面,count 方法只是跟踪匹配元素的数量,不关心元素本身,所以更高效。

要找到一个满足判断式的元素,使用 find 函数 :

val personList = listOf(Person("fanda", 38), Person("liuhang", 22))
println(personList.find(canBeInClub27))  // Person(name=liuhang, age=22)

如果有多个匹配的元素,就返回其中第一个元素,没有匹配的元素则返回 nullfind 还有一个同义方法 firstOrNull ,可以使用这个方法更清楚地表达你的意图。

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

假设你需要把所有元素按照不同的特征划分成不同的分组。例如,你想把人按年龄分组,相同的年龄的人在一组。把这个特征直接当做参数传递十分方便。 groupBy 函数可以帮你做到这一点:

val personList = listOf(Person("fanda", 38), Person("liuhang", 22), Person("dudu", 22))

println(personList.groupBy { it.age })

// 输出 {38=[Person(name=fanda, age=38)], 22=[Person(name=liuhang, age=22), Person(name=dudu, age=22)]}

结果是一个 map ,是元素分组依据的键(上述例子是 age) 和元素分组 (persons) 之间的映射。每一个分组都存在一个列表中,上述例子的结果的类型其实就是Map<Int,List<Person>> 。还可以使用像 mapKeysmapValues 这样的函数对这个 map 做进一步修改。比如:

println(personList.groupBy { it.age }.mapValues {
    it.value.joinToString { p: Person -> p.name }
})

// 输出 {38=fanda, 22=liuhang, dudu}

我们再来看另外一个例子,如何使用成员引用把字符串按照首字母分组:

val list = listOf("a", "ab", "dad", "rs")
// {a=[a, ab], d=[dad], r=[rs]}
println(list.groupBy(String::first))

这里的 first 并不是 String 类的成员,而是一个扩展,也可以把它当做成员引用访问。

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

flatMap 函数做了两件事:首先根据作为实参给定的函数对集合中单个元素做变换,然后把多个列表平铺成一个列表,示例如下:

data class Book(val title: String, val authors: List<String>)

val books = listOf(
    Book("a", listOf("aaa1", "aaa2")),
    Book("b", listOf("bbb1", "bbb2")),
    Book("c", listOf("ccc1", "ccc2")),
    Book("d", listOf("aaa1", "aaa2"))
)

// 只做了变换操作,[[aaa1, aaa2], [bbb1, bbb2], [ccc1, ccc2], [aaa1, aaa2]]
println(books.map { it.authors })
// 做了变换且平铺 [aaa1, aaa2, bbb1, bbb2, ccc1, ccc2, aaa1, aaa2]
println(books.flatMap { it.authors })
// 平铺且去重 [aaa1, aaa2, bbb1, bbb2, ccc1, ccc2]
println(books.flatMap { it.authors }.toSet())

flatMap 函数做了 map 的变换且还执行了平铺操作,toSet 函数是转换成 set 来进行去重处理了。

当你卡壳在元素集合的集合不得不合并成一个的时候,可以用 flapMap 来处理。如果你不需要做任何变换,只是需要平铺一个集合,可以使用 flatten 函数:

val list = listOf(listOf("aaa", "bbb", "ccc"), listOf("111", "222"), listOf(1, 2, 3))
// [aaa, bbb, ccc, 111, 222, 1, 2, 3]
println(list.flatten())

flatten 函数只作用在集合的集合中,没有变换处理,结果会平铺成一个列表。

惰性集合操作:序列

先来看个例子:

val personList = listOf(Person("fanda", 38), Person("liuhang", 22), Person("dudu", 22))
val newList = personList.map(Person::name).filter { it.startsWith("f") }
println(newList)

filtermap 都会返回一个列表。这意味着上面例子中的链式调用会创建两个列表:一个保存 filter 函数的结果,另一个保存 map 函数的结果。如果原列表只有两个元素,这不是什么问题,但是如果有一百万个元素,链式调用就会变得十分低效。

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

val sequenceList =personList.asSequence()       // 把初始集合转换成序列
    .map(Person::name).filter { it.startsWith("f") }
    .toList()   // 把结果序列转换回列表

序列没有创建任何用于存储元素的中间集合,所以元素数量巨大的情况下性能将显著提升。可以调用扩展函数 asSequence 把任意集合转换成序列,调用 toList 来做反向的转换。因为序列的操作是惰性的,为了执行它们,你需要直接迭代序列元素,或者把序列转换成一个集合。

注意: 需要对一个大型集合执行链式操作时使用序列才有优势,通常都不需要使用序列。

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

序列操作分为两类:中间的和末端的。一次中间操作返回的是另一个序列,这个新序列知道如何变换原始序列中的元素。而一次末端操作返回的是一个结果,这个结果可能是集合、元素、数字,或者其他从初始集合的变换序列中获取的任意对象。

                    //中间操作         //末端操作
personList.asSequence().map{..}.filter {..}.toList() 

中间操作始终都是惰性的。先看看下面这个缺少了末端操作的例子:

listOf(1, 2, 3, 4).asSequence()
    .map { print("map($it) "); it * it }
    .filter {
        print("filter($it) "); it % 2 == 0
    }

注意: lambda 多行代码写在同一行时,要用 ; 分隔开,最后的代码才是结果,不然就写成多行的。

listOf(1, 2, 3, 4).asSequence().map {
    // 写成多行
    print("map($it) ")
    it * it
}.filter {
    print("filter($it) ")
    it % 2 == 0
}

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

listOf(1, 2, 3, 4).asSequence()
    // 写在一行,要有分号 ;
    .map { print("map($it) "); it * it }
    .filter {
        print("filter($it) "); it % 2 == 0
    }.toList()  // 末端操作

// 输出 
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16) 

末端操作触发执行了所有的延期计算。
这个例子中另外一件值得注意的事情是计算执行的顺序。一个笨办法是现在每个元素上调用 map 函数,然后在结果序列的每个元素上在调用 filter 函数。 mapfilter 对集合就是这样做的,而序列不一样。对序列来说,所有操作是按顺序应用在每一个元素上的,处理完第一个元素(先映射在过滤),然后完成第二个元素的处理,以此类推。这种方法意味着部分元素根本不会发生任何变换,如果在轮到它们之前就已经取得了结果。

创建序列

前面的列表都是使用同一个方法创建序列:在集合上调用 asSquence() 。另一个可能性是使用 generateSequence 函数。给定序列中的前一个元素,这个函数会计算出下一个元素。下面这个例子就是如何使用 generateSequence 计算 100 以内所有自然数之和。

val numbers = generateSequence(0){ it + 1}  // 生成一个序列
val numbersTo100 = numbers.takeWhile { it <= 100 }  // 中间操作,生成另一个序列
println(numbersTo100.sum()) // 末端操作,执行所有被推迟的操作,得到结果

注意:这个例子中的 numbers 和 numbersTo100 都是有延期操作的序列。这些序列中的实际数字直到你调用末端操作(这里是sum)的时候才会求值。

另一种常见的用例是父序列。如果元素的父元素和它的类型相同(比如人类或者Java文件),你可能会对它所有祖先组成的序列的特质感兴趣。下面这个例子可以查询文件是否放在隐藏目录中,通过创建一个其父类目录的序列并检查每个目录的属性来实现。

fun File.isInsideHiddenDirectory() =
    generateSequence(this) { it.parentFile }.any { it.isHidden }

val file = File("/Users/svtk/.HiddenDir/a.txt")
println(file.isInsideHiddenDirectory())

你生成一个序列,通过提供第一个元素获取每个后续元素的方式来实现。如果把 any 换成 find ,你还可以得到想要的那个目录(对象)。注意,使用序列允许你找到需要的目录之后立即停止遍历目录

使用 Java 函数式接口

Kotlinlambda 也可以无缝地和 Java API 互操作。在文章开头,我们就把 lambda 传给 Java 方法的例子:

/* Kotlin */
button.setOnClickListener{ /* do someting */ }

Button 类通过接收类型为 OnClickListner 的实参的 setOnClickListener 方法给按钮设置一个新的监听器,在Java 8 之前中我们不得不创建一个匿名类来作为实参传递:

/* Java */
button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //do something
        }
});

Kotlin 中,可以传递一个 lambda ,代替这个例子:

/* Kotlin */
button.setOnClickListener{ view -> ... }

这种方法可以工作的原因是 OnClickListener 接口只有一个抽象方法,这种接口被称为函数式接口或者 SAM 接口(单抽象方法接口),Java API 中随处可见像 RunnableCallable 这样的函数式接口,以及支持它们的方法。 Kotlin 允许你在调用接收函数式接口作为参数的方法时使用 lambda ,来保证你的 Kotlin 代码即整洁又符合习惯。

把lambda当做参数传递给Java方法

可以把 lambda 传给任何期望函数式接口的方法。例如下面这个方法,它有一个 Runnable 类型的参数:

// java
public static void postponeCoputation(int delay, Runnable computation) {
    computation.run();
}

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

LambdaPraticeJava.postponeCoputation(1000) { println(42) }

当我们说一个 Runnable 的实例时,指的是一个实现了 Runnable 接口的匿名内部类的实例,编译器会帮你创建它,并使用 lambda 作为单抽象方法的方法体。

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

LambdaPraticeJava.postponeCoputation(1000, object : Runnable {
    override fun run() {
        // 这里打印 hashCode
        println(this.hashCode())
    }
})

这里打印的是匿名内部类的 hashCode ,通过 this 来引用该类,上述没有显式创建对象的 lambda 是引用不了 this 的。

注意:当你显式地声明对象时,每次调用都会创建一个新的实例。而如果 lambda 没有访问任何来自定义它的函数的变量,相应的匿名类实例可以在多次调用之间重用。

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

val runnable = object : Runnable {
    override fun run() {
        println( this.hashCode())
    }
}

测试用例:

fun testLabdaMethod2() {
    for (count in 1..3) {
        LambdaPraticeJava.postponeCoputation(1000, object : Runnable {
            override fun run() {
                println(this.hashCode())
            }
        })
    }

    println()

    for (count in 1..3) {
        // 每次调用的时候都使用这个变量
        LambdaPraticeJava.postponeCoputation(1000, runnable)
        // 等价于这种形式调用
        LambdaPraticeJava.postponeCoputation(1000) { ... }
    }
}

输出:

1392838282
523429237
664740647

// 数值一样,证明相应的匿名类实例在多次调用之间重用
testValue804564176
testValue804564176
testValue804564176

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

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

SAM 构造方法是编译器自动生成的函数,让你执行从 lambda 到函数式接口实例的显式转换。例如,如果有一个方法返回的是一个函数式接口的实例,不能直接返回一个 lambda ,要用 SAM 构造方法把它包起来:

fun createRunnable() = Runnable { println("Create Success") }

createRunnable().run()

SAM 构造方法的名称和底层函数式接口的名称一样,参数只有一个(被用作函数式接口单抽象方法体的 lambda ),返回的是这个接口类的一个实例。

除了返回值外,SAM 构造方法还可以用在需要把从 lambda 生成的函数式接口实例存储在一个变量中的情况。假设你要在多个按钮上重用同一个监听器,就像下面的代码一样:

val listener = View.OnClickListener{ view ->
    val text = when (view.id) {
        R.id.button_1 -> "First Button"
        R.id.button_2 -> "Second Button"
        else -> "Unknown Button"
    }
    println(text)
}

firstButton.setOnClickListener(listener)
secondButton.setOnClickListener(listener)

其实也可以用对象表达式来创建实例,但是 SAM 构造方法更简洁。

注意: lambda 内部没有匿名对象那样的 this ,即没有办法引用到 lambda 转换成的匿名类实例。从编译器的角度来看,lambda 是一个代码块,不是一个对象,而且也不能把它当成对象引用。 lambda 中的 this 指向的是包围它的类。如果你的事件监听器在处理事件时还需要取消它自己,不能使用 lambda 这样做。这种情况使用实现了接口的匿名对象,在匿名对象内,this 关键字指向该对象实例,可以把它传给移除监听器的 API

带接收者的 lambda : with 与 apply

with 函数

可以用 with 函数对同一个对象执行多次操作,而不需要反复把对象的名称写出来,我们先看一个不用 with 函数的例子:

fun testNoWithMethod(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\nEnd")
    return result.toString()
}

上述的例子中,调用了 result 实例的好几个方法,而且每次调用都要重复 result 这个名称,很麻烦且不简洁,用 with 函数重写后是这样的:

fun testWithMehtod() =
        with(StringBuilder()){
            for (letter in 'A'..'Z') {
                this.append(letter)    // 显式调用 this
            }
            append("\nEnd")    // 省略 this
            toString()    // 从 lambda 返回值
        }

现在这个函数只返回一个表达式,所以使用表达式函数体语法重写了它。

with 函数实际上是一个接收两个参数的函数:这个例子中两个参数分别是 stringBuilder 和一个 lambda 。这里利用了把 lambda 放在括号外的约定,这样整个调用看起来就像是内建的语言功能,当然你也可以选择把它写成 with(stringBuilder, {...})with 函数把它的第一个参数转换成第二个参数传给他的 lambda 的接收者。可以显式地通过 this 引用来访问这个接收者,或者可以省略 this 引用。
with 返回的值是执行 lambda 代码的结果,该结果就是 lambda 中的最后一个表达式(的值)。

可以使用标准库函数 buildString 进一步简函数 :

fun testBuildString() = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nEnd")
}

它会负责创建 StringBuilder 并调用 toString,源码是这样的:

public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
    StringBuilder().apply(builderAction).toString()

注意:在扩展函数体内部,this指向了这个函数的那个类型的实例,而且也可以被省略掉,让你直接访问接收者的成员。一个扩展函数某种意义上来说就是带接收者的函数。

注意: 如果使用 with 的类的方法名和外部的对象的名称一样,可以使用 this@OuterClass 这样的语法来引用外部类的方法。

apply 函数

apply 函数跟 with 函数的唯一区别是始终会返回作为实参传递给它的接收者对象,让我们用 apply 来重构上述的方法:

fun testApplyMethod() =
    StringBuilder().apply {
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nEnd")
    }.toString()

在创建一个对象实例需要用正确的方式初始化它的一些属性的时候。在 Java 中,这通常是通过另外一个单独的 Builder 对象来完成的,而在 Kotlin 中,可以在任意对象上使用 apply ,完全不需要任何任何来自定义该对象的库的特别支持。

我们来用 apply 演示一个 Android 中创建 TextView 实例的例子:

fun createViewWithCustomAttr(context: Context) =
    TextView(context).apply {
        text = "simple text"
        textSize = 19f
        setPadding(0, 10, 0, 10)
    }

Lambda 执行之后,apply 返回已经初始化过的接收者实例,它变成了 createViewWithCustomAttr 函数的结果。

小结

  • lambda 允许你把代码块当作参数传递给函数。
  • kotlin 可以把 lambda 放在括号外传递给函数,而且可以用 it 引用单个 lambda 参数。
  • lambda 中的代码可以访问和修改包含这个 lambda 调用的函数中的变量。
  • 通过在函数名称前加上前缀 :: ,可以创建方法、构造方法及属性的引用,并用这些引用代替 lambda 传递给函数。
  • 使用像 filtermapallany 等函数,大多数公共的集合操作不需要手动迭代元素就可以完成。
  • 序列允许你合并一个集合上的多次操作,而不需要创建新的集合来保存中间结果。
  • 可以把 lambda 作为实参传给接收 java 函数式接口(带单抽象方法的接口,也叫作 SAM 接口)作为形参的方法。
  • 带接收者的 lambda 是一种特殊的 lambda ,可以在这种 lambda 中直接访问一个特殊接收者对象的方法。
  • with 函数允许你调用一个对象的多个方法,而不需要反复写出这个对象的引用。
  • apply 函数让你使用构建者风格的 API 创建和初始化任何对象。