前言
lambda表达式本质上就是可以传递给其他函数的一小段代码,kotlin中大量使用了它们,最差个间的一种lambda用途就是和集合一起工作。
Lambda表达式和成员引用
lambda简介:作为函数参数的代码块
你常常需要表达这样的想法:当一个事件发生的时候运行这个事件处理器。或者把这个操作应用到这个数据结构中所有的元素上。在老版本java中可以用匿名内部类实现,但是太啰嗦了。
函数式编程提供了一种方法:把函数当作值来对待。可以直接传递函数,不需要先声明一个类再传递这个类的实例。它可以高效地直接传递代码块作为函数参数
在java中
在kotlin中
这样一句直接代替了java中的匿名内部类的写法
Lambda和集合
假设我们有一个Person的列表,需要找到列表中年龄最大的那个人,在不了解java时我们会去迭代列表返回一个保存好了的变量
而在kotlin中有更好的方法
maxByOrNull函数可以在任何集合上调用,且只需要一个实参:一个函数,指定比较哪个值来找到最大元素。花括号中{it.age}就是实现了这个逻辑的lambda。它接受一个集合中的元素作为实参(使用it 引用它)并且返回用来比较的值。在上面的例子中,Person是集合元素,用来比较的是存储在age属性中的年龄值。
如果lambda刚好是函数或者属性的委托,可以用成员引用替换
Lambda表达式的语法
一个lambda把一小段行为进行编码,你能把它当作值到处传递,它可以被独立地声明并存储到一个变量中,但更常见的是直接声明它并传递给函数 表达式始终用花括号包围,实参没有用括号括起来,箭头把实参和函数体隔开 可以把lambda表达式存储在一个变量中,这个变量当作普通函数对待,可通过相应实参调用它
但这样没什么意义,等价于直接执行lambda函数体中的代码。如果确实需要把一小段代码封闭在一个代码块中,可以使用库函数run来执行传给它的lambda
在前面在集合中寻找年龄最大的人的语句最完整的样子如下
含义:把花括号的代码片段作为实参传给函数,这个lambda接受一个类型为Person的参数并返回它的年龄
这样写过于啰嗦,kotlin有一个语法约定:如果lambda表达式是函数调用的最后一个实参,它可以放到括号的外边。在这里lambda是唯一的实参,可以放到括号后面
唯一实参时还可以去掉调用代码中的空括号对
三种形式含义都是一样的,如果你想传递两个或更多的lambda,不能把超过一个的lambda放到外面,这时使用常规语法来传递它们通常是更好的选择。
把lambda作为命名实参传递
重写该调用,把lambda放在括号外
省略lambda参数类型
和局部变量一样,如果lambda参数的类型可以被推导出来,就不需要显式指定它。以maxByOrNull为例,其参数类型始终和集合的元素类型相同有,编译器知道你是对一个Person对象的集合调用maxByOrNull函数,所以他能推断lambda参数也会是Person类型
最后的简化,仅在实参名称没有显式指定时这个默认的名称才会生成(如果当前上下文期望的是只有一个参数的lambda且这个参数类型可以推断出来)
注意!it 不能滥用,尤其是在嵌套lambda的情况下,最好显式声明每个lambda的值
如果用变量存储lambda,那么就没有可以推断出参数类型的上下文,所以必须显式指定参数类型
lambda可以包含更多的语句
在作用域中访问变量
当在函数内声明一个匿名内部类的时候,能够在这个匿名类内部引用这个函数的参数和局部变量,如果在函数内部用lambda也可以访问这个函数的参数,还有在lambda之前定义的局部变量。
在lambda中使用函数参数
在kotlin中不会仅限于访问final变量,在lambda内部也可以修改这些变量
在lambda中改变局部变量
kotlin允许在lambda内部访问非final变量甚至修改它们。从lambda内访问外部变量,我们就称这些变量被lambda捕捉
默认情况下,局部变量的生命期被限制在声明这个变量的函数中,但如果它被lambda捕捉了,使用这个变量的代码可以被存储并稍后再执行。当你捕捉final变量时,他的值和使用这个值的lambda代码一起存储,对非final变量来说,它的值被封装在一个特殊的包装器中,这样你就可以改变这个值,而对这个包装器的引用会和lambda代码一起存储
捕捉可变变量:实现细节
java只允许你final变量,当你想捕捉可变变量的时候,要么声明一个单元素的数组,其中存储可变值,要么创建一个包装类的实例,其中存储要改变的值的引用
在kotlin中显式使用这些技术
在实际代码中不需要包装器,可以直接修改这个变量,上方就是原理,当你捕捉了一个可变变量时,它的值被作为包装类的一个实例而被存储下来,包装类变量是final类型可以被轻易捕捉,然而实际值被存储在其字段中,并且可以在lambda内修改
注意! 如果lambda被用作事件处理器护着用在其他异步执行的情况,对局部变量的修改只会在lambda执行的时候发生
这个函数始终返回0,我们无法观察到值的变化,因为监听器处理器是在函数返回后调用的,这个函数的正确实现应把点击次数存储在函数外依然可以访问的地方,而不是存储在函数的局部变量中。
成员引用
如果你想要当作参数传递的代码已经被定义成了函数…
kotlin和java8一样,把函数转换成一个值,你就可以传递他,使用:: 运算符来转换
这被称为成员引用,它简明地创建一个调用单个方法或者访问单个属性的函数值,双冒号把类名称与你要引用的成员隔开。不管你引用的是函数还是属性,都不要在成员引用的名称后面加括号。成员引用和调用该函数的lambda具有一样的类型,所以可以互换使用。
还可以引用顶层函数 形式如下 ::函数名,run(::a)此时省略了类名称,::函数名被当作实参传递给库函数run,它会调用相应的函数。
如果lambda要委托给一个接收多个参数的函数,提供成员引用代替它将会非常方便
这个lambda委托给send参数,也可以用成员引用代替
可以用构造方法引用存储或者延期执行创建类实例的动作。构造方法引用的形式是在双冒号后指定类名称
还可以用同样的方式引用扩展函数,即使他不是Person类的成员,但还是可以访问,和访问实例的成员一样
绑定引用
在kotlin1.1版本中支持绑定成员引用,它允许你使用成员引用语法捕捉特定实例对象上的方法引用
在1.1之前你需要显式写出lambda{p.age}而不是使用绑定成员引用p::age
集合的函数式API
基础:filter和map
它们形成了集合操作的基础,很多集合操作都是借助它们表达的
filter 函数遍历集合并选出应用给定lambda后会返回true的那些元素,输出结果的集合中只包含输入集合中那些满足判断式的元素,但它不会改变这些元素。
map 函数对集合中的每一个元素应用给定的函数并把结果收集到一个新集合
返回一个它们的平方集合
打印一个姓名列表并改为成员引用
把多次这样的调用链接起来,下图为输出年龄大于30岁的人的名字
当需要这个分组中所有年龄最大的人的名字时
注意!这段代码对每个人都会去找一遍最大年龄,而不是只计算一次最大年龄
改进:把计算最大年龄用变量提取出来,然后再用其和每一个人比较
all、any、count、find:对集合应用判断式
另一种常见的任务是检查集合中所有元素是否都符合某个条件(或是否存在符合的元素),它们通过all 和any 函数表达。conut 函数检查有多少元素满足判断式,find 函数返回第一个符合条件的元素。
先定义一个判断式isless18,若你对所有的元素是否满足判断式感兴趣,那就可以使用all ,如果只需要检查集合中至少存在一个匹配的元素就使用any
如果你想知道有多少个元素满足了判断式,则使用count
要找到一个满足判断式的元素,使用find函数,若有多个匹配的,返回第一个,没有一个满足返回null。同义方法firstOrNull
groupBy:把列表转换成分组的map
若你需要把所有元素按照不同的特征划分成不同的分组,比如把人按年龄分组
每一个分组都是存储在一个列表中,结果的类型就是Map<Int,List< Person>>
使用成员引用把字符串按照首字母分组
flatMap和flatten:处理嵌套集合中的元素
每本书都可能有一个或者多个作者,可以统计处图书馆中的所有作者的set
这样子books集合中书籍的所有作者的set便创建成功,toSet()会移除重复元素
flatMap 做了两件事 根据作为实参给定的函数对集合中的每个元素做变换(或者说映射),然后把多个列表合并(平铺)成一个列表
例子
第一步映射,把abc和def都映射成单个字符,第二步把它们平铺,结合在一起成为一个列表
字符串上的toList函数把它转换成字符列表
若你不需要做任何变换,只是需要平铺一个集合,可以使用flatten 函数
|