定义
Lambda
表达式,本质上就是可以传递其他函数的一小段代码。有了lambda
,可以轻松的把通用的代码结构抽取成库函数。
1.Lambda表达式语法
{ x: Int, y: Int -> x + y }
- lambda表达式由花括号包围
- 实参和lambda表达式由
->
分隔开 -
->
前面是实参,这里的实参不需要用括号括起来
可以把lambda表达式存储在一个变量中,并将这个变量当做普通函数调用使用
val sum = { x: Int, y: Int -> x + y }
println(sum(1,2))
可以用库函数run来执行传递给他的lambda
run { println(42) }
2.Lambda表达式使用场景
-
作为函数的参数传递
在开发中,我们经常需要用一段代码存储和传递一小段行为。比如:安卓中的按钮点击事件的监听,我们用匿名内部类的方式来实现:
这种方式实现的事件的监听,每一次我们都需要声明一个类并且传递这个类的实例,语法啰嗦。
button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //点击后执行的动作 } });
这种方式是Kotlin中的实现,把函数当做值来处理,使用lambda表达式使语法更简洁。
button.setOnClickListener { }
-
Lambda的集合操作
由于lambda的帮助,在Kotlin中对集合很多的操作都被封装在了库里,避免了很多重复代码。
通过将一个lambda表达式传递给maxBy库函数,轻松地找出了集合中年龄最大Person对象
val persons = listOf(Person(1,"tom"),Person(2,"jack"), Person(200,"joey")) println(persons.maxBy { it.age })
如果lambda刚好是函数或者属性的委托,可以用成员引用替换
persons.maxBy(Person::age)
3.lambda语法的简化
以下代码是未经过任何简化的语法:花括号中的代码片段是lambda表达式,他作为实参传递给maxBy函数。这个lambda表达式接受一个Person类型的对象,并返回他的年龄。
persons.maxBy({ p: Person -> p.age })
在Kotlin中有几个约定,可以用来简化lambda表达式
-
如果lambda表达式是函数的最后一个实参,它可以放到括号外边
persons.maxBy() { p: Person -> p.age }
-
当lambda是函数的唯一实参时,可以去掉代码中的空括号
persons.maxBy { p: Person -> p.age }
-
如果lambda表达式参数的类型可以被推导出来,可以省略参数类型
persons.maxBy { p -> p.age }
-
使用默认参数名it代替命名参数(如果上下文期望的只有一个参数的lambda且参数的类型可以推断出来,就会生成这个名称)
persons.maxBy { it.age }
4.在作用域中访问变量
在函数的内部使用lambda时,可以访问这个函数的参数
fun printDataWithPrefix(datas: Array<String>, prefix: String) {
datas.forEach {
println("$prefix $it")
}
}
在函数的内部使用lambda时,可以访问在lambda前定义的局部变量
//统计传入集合中,可以 对2取余等于0和不等于0的个数
fun getOddCounts(nums: List<Int>) {
//定义局部变量
var oddCounts = 0
var normalCounts = 0
nums.forEach {
if (it % 2 == 0) {
oddCounts++
} else {
normalCounts++
}
}
println("oddCounts is $oddCounts,normalCounts is $normalCounts")
}
与Java不一样的是,Kotlin中的在lambda内部可以访问非final变量,甚至可以修改他们。我们称这些变量被lambda捕捉
。
5.成员引用
Kotlin中可以把函数转换成一个值,这样我们就可以传递一个普通的函数。
Fruit::color
fruits.maxBy(Fruit::color)
这种表达式叫做成员引用
,可以创建一个调用单个方法或者访问单个属性的函数值。使用时,用::把类名称和要引用的成员分开。
- 成员引用还可以用作引用顶层函数
fun topPrint() {
println("顶层函数调用")
}
run(::topPrint)
- 如果lambda要委托给一个接受多个参数的函数,提供成员引用代替它会非常方便
val action = { fruit: Fruit, message: String -> sendEmail(fruit, message) }
val nextAction = ::sendEmail
- 可以用
构造方法引用
存储或者延迟执行创建类实例的动作
//构造方法引用:在类名前加双引号 ::
val plantFruit = ::Fruit
val f = plantFruit(20)
- 可以用成员引用的方式访问扩展函数
fun Fruit.isRed() = color > 15
val test = Fruit::isRed
6.集合的函数式API
函数式编程风格在操作集合时提供了很多优势,大多数任务可以通过库函数完成,来简化代码
filter
函数:
遍历集合并选出应用给定lambda后会返回true的那些元素
//使用filter函数,返回偶数集合
val datas = listOf(1, 2, 3, 4, 5)
println(datas.filter { it % 2 == 0 })
filter
函数可以从集合中过滤我们不想要的元素,但是并不会改变这些元素
map
函数:
- 对集合中的每一个元素应用给定的函数,并将结果收集到一个新集合。
//将传入的数字列表,变成他们的平方的列表
val list = listOf(1, 2, 3)
println(list.map { it * it })
- 可以对map应用过滤和变换操作
//过滤掉map中 key < 1的元素
val maps = mapOf(1 to "one", 2 to "two", 3 to "three")
println(maps.filterKeys { it > 1 })
filterKeys
和mapKeys
过滤和变换map的键
filterValues
和mapValues
过滤和变换map的值
all any count find
函数
用于检查集合中元素是否符合某个条件
-
all
函数判断是否所有元素满足判断式//定义age >= 18对可以吸烟的判断式 val canSmoke = { h: Humen -> h.age >=18 } //检查是否所有人都可以吸烟 println(humens.all(canSmoke))
-
any
函数检查集合中是否至少存在一个匹配的元素//判断是否有可以吸烟的人 println(humens.any(canSmoke))
-
find
函数返回第一个符合条件的元素//查找第一个可以吸烟的人 println(humens.find(canSmoke))
-
count
函数检查有多少元素满足判断式//统计可以吸烟的客户 println(humens.count(canSmoke))
groupBy
函数
把所有元素按照不同的特征划分成不同的组
val fruits = listOf(Fruit(1), Fruit(2))
println(fruits.groupBy { it.color })
>> {1=[Fruit(color=1)], 2=[Fruit(color=2)]}
groupBy
函数的结果是一个map
,是元素分组依据的键和集合元素之间的映射。
flatMap
函数
处理嵌套集合中的元素:首先根据作为实参给定的函数对集合中的每个元素做变换
(或者说映射
),然后把多个列表合并(或者说平铺
)成一个列表。
val strings = listOf("ac", "abcf", "your")
println(strings.flatMap { it.toList() })
>> [a, c, a, b, c, f, y, o, u, r]
7.惰性集合操作:序列
- 序列使用场景
在对集合进行链式操作时,这些函数操作会创建中间集合,每一步的结果都会存在一个临时列表中。当集合元素很多时,这样的调用会是很低效的。
books.map(Book::title)
.filter { it.length > 2 }
为了提高效率,可以把集合变成序列。
- 这种方式不会创建储存元素的中间变量
- 用扩展函数
asSequence
把任何集合转换成序列,调用toList
做反向转换 - 通常对大型集合进行链式操作时,要是用序列
books.asSequence()
.map (Book::title)
.filter { it.length > 2 }
.toList()
-
序列的操作
序列的操作分为:中间操作和末端操作
中间操作
:对序列执行的一系列变换操作,返回另一个序列,这个新序列知道如何变换原始序列中的元素。中间操作是惰性的。//以下代码缺少了末端操作。执行后没有任何打印,证明中间操作未执行 books.asSequence() .map { println(it.title) }
末端操作
:返回一个结果,这个记过可能是集合、元素、数字,或者从初始化集合的变换序列中获取的任意对象。只有当末端操作被调用时,中间操作才会执行//上述代码加入了 末端操作:toList books.asSequence() .map { println(it.title) } .toList() >> Java Kotlin实战
对序列来说,所有操作是按照顺序应用在每一个元素上:处理完第一个元素(先映射再变换),然后再处理第二个元素。如果轮到某些元素时已经取到结果,其后面的元素就不会再发生任何变换。
//序列中,第二个元素2变换为4 > 3,此时已经找到了满足的值。剩余的元素就不再变换了
listOf(1, 2, 3, 4).asSequence()
.map { println(it * it); it * it }
.find { it > 3 }
>> 1
4
//对集合操作时,每个元素都经过了map变换操作
listOf(1,2,3,4)
.map { println(it * it); it * it }
.find { it > 3 }
>> 1
4
9
16
对集合执行操作时,先使用filter
再使用map
有助于减少变换总次数。如果map在前,每个元素都会发生变换。
-
创建序列
使用
generateSequence
可以创建一个序列。通过给定序列的第一个元素,和获取每个后续元素的方式来实现//指定前一个元素为0,并给定规则,创建出0~100的数字序列 val numberTo100 = generateSequence(0) { it + 1 }.takeWhile { it <= 100 } //对序列进行求和 println(numberTo100.sum())
8.使用Java函数式接口
在使用Kotlin时,我们可以将Lambda传递给Java方法。Kotlin的Lambda可以无缝的和Java API互操作。
在Java中,我们需要创建一个匿名类的实例作为实参传递给setOnClickListener
,实现点击的监听
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//点击后执行的动作
}
});
Kotlin中,我们可以传递一个Lambda来代替这个实例
button.setOnClickListener{ view -> println("按钮点击") }
这个lambda接受一个View类型的参数,和OnClick方法一样
SAM类型接口
能够直接传递lambda的原因是:OnClickListener
是一个SAM类型的接口,即只有一个抽象方法的接口,又称为函数式接口。定义了这种类型的接口,使得以其为参数的的方法,可以在调用时,传递一个lambda表达式作为参数。
-
把lambda当做参数传递给Java方法
可以把lambda传给任何期望函数式接口的方法,编译器会把lambda自动转换为一个实现了当前SAM接口的匿名类的实例,并且将lambda作为SAM接口单抽象方法的方法体
//这个方法需要传递一个Runnable类型的参数
public static void postponeComputation(int delay,Runnable runnable){
}
//Kotlin中调用时,就可以直接传递一个lambda作为实参,编译器会自动把它转换成一个Runnable实例
Test.postponeComputation(100){
println("print")
}
通过显式的创建一个匿名对象也能达到同样的效果,但是这样每次调用都会创建一个新的匿名类的实例
Test.postponeComputation(1000, object : Runnable {
override fun run() {
}
})
使用lambda表达式,创建的匿名类实例可以在多次调用之间复用
Test.postponeComputation(100){
println("print")
}
//以上代码等价于 显式声明一个Runnable的实例并存储在一个变量中,则每次调用时使用就是这个实例
val runnable = Runnable { println("print") }
Test.postponeComputation(100,runnable)
当有变量被lambda表达式捕获时,每次调用将不再重用同一个实例了。这种情况下,每次编译器都会创建一个新对象,其中存储着变量的值。
fun caculateResult(number: Int) {
Test.postponeComputation(10000) {
println(number.toString())
}
}
-
SAM接口的构造器,将lambda显式的转换为函数式接口
SAM接口的构造器是编译器提供的,他能让你显式的将lambda表达式显式的转换为一个函数接口的实例。当编译器不能自动完成lambda到函数式接口的转换时,可以使用这种显式的转换方式。
//这个函数需要返回函数式接口,但是不能直接返回lambda,我们需要将它包裹在SAM的构造器中 fun createSpecificRunnable(): Runnable { return Runnable { println("runnable") } }
SAM接口构造器的名字和接口的名字相同。构造器只有一个参数----一个lambda表达式,它会被用做SAM接口的单抽象方法的方法的方法体---并且返回一个实现了这个接口的实例。
除了返回值以外,构造器还可以用在要将由lambda生成的函数式接口保存在变量中时.下面代码中把通过构造器将SAM接口保存在了listener变量中,达到重用的效果。
val listener = View.OnClickListener { v -> val text = when (v.id) { R.id.editText -> "text1" R.id.editText2 -> "text2" R.id.editText3 -> "text3" else -> "unknown" } Toast.makeText(context,text,Toast.LENGTH_SHORT).show() }
-
带接受者的lambda
with
和apply
在lambda中,不使用任何附加的修饰符就能调用不同对象的方法。这种lambda叫做带接收者的lambda
with
函数许多语言中都有特定的语句,能让你在对一个对象执行多次操作时,不用重复写对象的名字来进行操作。Kotlin中以库函数的形式来提供,
with
函数fun alphbet(): String { val stringBuilder = StringBuilder() return with(stringBuilder) { for (letter in 'a'..'z') { this.append(letter) } this.toString() } }
上面的with
函数 接受两个参数
- stringBuilder
- 一个lambda
with函数把它第一个参数转换成作为第二个参数传给它的lambda的接收者
。便可以显式的通过this引用来访问这个接收者
apply
函数
apply函数几乎和with函数一样,不同的是apply函数返回的作为参数传递给它的对象。
//with函数返回的StringBuilder类型,调用toString()转换成String
fun alphbet2(): String = StringBuilder().apply {
for (letter in 'a'..'z') {
this.append(letter)
}
}.toString()