Kotlin 学习之lamabda表达式
一.lamabda初体验
1.假如现在有个需求,需要从一个集合中找出对应的最大的元素
例如找出person类的集合中年龄最大的人,person类如下:
class Person(var name : String,var age : Int) {
override fun toString(): String {
return "Person(name=$name,age=$age)"
}
}
如果用普通的方式,你可能需要遍历集合,判断年龄,然后比较,得出年龄最大的那个人.
假如现在用集合的库函数maxBy.该函数正好接收一个lamabda表达式,maxBy()函数的定义如下:
fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) -> R): T?
从函数lamabda表达式中可以看到,它会接收一个泛型T作为实参,也就是集合的泛型T,运行结果是一个比较器类型,并且max函数本身返回一个泛型T.
现在我们调用maxBy()尝试下:
fun main(args: Array<String>) {
val personList = listOf(Person("aaa", 12), Person("bbb", 13))
println(personList.maxBy{it.age})
}
对,就是这样看起来简单,方法中的it代表person对象,也就是上面的T,当函数接收参数只有一个时,可以用it关键字指代,当然你要写成这样也是可以的:
println(personList.maxBy { person : Person -> person.age })
并且,lamabda还有一种更简便的形式,当表达式刚好是函数或者属性的委托,可以用成员引用替换,就像这样:
println(personList.maxBy(Person::age))
注意,这里是括号,而非中括号.
二.lamabda定义
lamabda就是一小段代码的编码形式,你可以称之为一个代码块,因为它总是被一对中括号括起来.它也是一个表达式,所谓表达式就是有值得一个代码块,可以被变量所引用.不过最常见的还是被当做函数参数传递接收lamabda表达式的函数.具体形式如下图:
image.png
它由参数指向函数里,参数也就是需要传递的或者接收的实参
3.lamabda语法与简化
回到上面那个例子,如果说最普通的方式调用maxBy函数,应该是这样,就像函数的定义一样:
println(personList.maxBy ({person : Person -> person.age}))
但是lamabda表达式规定当函数参数是lamabda表达式并且lamabda表达式作为函数参数的最后一个参数时可以移到括号的外面,就像这样;
println(personList.maxBy () {person : Person -> person.age})
如果lamabda是唯一的参数时,这个括号可以去掉,就像这样:
println(personList.maxBy { person : Person -> person.age })
就成了我们上文中提到的那种写法.
然后由于kotlin中的类型推导,可以去掉person的类型声明:
println(personList.maxBy { person -> person.age })
因为maxBy()函数中表达式的参数时泛型T,kotlin会自动推导出这个参数名称person的类型就是Person
然后如果这个lamabda只接受一个参数,且这个参数可以用类型推导推导出来,可以用it指代这个参数.就像上面那样写法.这个叫做it约定.
而且,lamabda不是只可以有一行语句,可以有多行,最后一行作为结果返回,就像这样:
println(personList.maxBy {
pritln("aaaaa")
person : Person -> person.age
})
三.lamabda在作用域中访问变量
lamabda表达式很多时候用于Java中匿名内部类表达形式,也就是说可以用lamabda表达式替换匿名内部类.
但是,这里需要注意的是,java中匿名内部类访问函数中的形参,或者函数定义的变量是要定义成final类型的,但是在lamabda中不需要,一样可以访问并且修改.就像这样:
fun forEachList(ageList: List<Int>) {
var prefix : String = ""
ageList.forEach {
prefix = "bbb"
print(it)
}
}
在lamabda中访问函数中定义的局部变量并修改其中的值,这种方式叫变量被lamabda表达式捕捉.而在java中只能捕捉final类型的变量
捕捉的定义:所谓捕捉就是如果这个变量是final类型的变量,它会和lamabda一起被保存下来稍后执行,如果这个类型是个可变变量,它会被一个类包装器引用起来,然后这个引用会和lamabda一起被保存下来,然后你就可以修改它的值.就像定义一个final类型的集合,集合是不可变的,但集合里面的元素是可变的
val myList : MutableList<Int> = mutableListOf()
myList.add(1)
成员引用
上文已经提到过成员引用的实例.
所谓成员引用就是当lamabda中调用的是一个方法或者一个属性值时可以用一种简明语法代替.例如上例中maxBy函数中访问的是person.age,那么此时age就是person中的一个属性.就如这样:
println(personList.maxBy(Person::age))
这里要注意: 不管是引用的成员还是函数,都不要在引用的名称后面加括号.
成员引用顶层函数:
fun book() {
println("KOTLIN")
}
run(::book)
如果一个lamabda需要把一个或者多个参数委托给一个函数,如下面这样:
val action = {student : Student,message : String ->
sendEmail(student,message)
}
这时候使用成员引用会非常方便:
val nextAction = ::sendEmail
此时::sendEmail等价于下面这个lamabda表达式:
{student : Student,message : String ->
sendEmail(student,message)
}
构造方法引用存储或者延期创建类的实例:
class Cap(name : String)
fun main(args: Array<String>) {
//创建对象被保存成一个值
val createCap = ::Cap
val cap = createCap("OCap")
}
还可以用来引用扩展函数:
fun Cap.readerName() {
println(name)
}
val readerCap = Cap::readerName
集合的库函数使用lamabda表达式
filter函数
filter顾名思义就是过滤的意思,过滤掉不想要的数据,filter的lamabda表达式中定义的事过滤条件,不符合条件的元素将会被剔除掉.如下条件:
val numberList = listOf(1,2,3,4,5,6)
val filterList = numberList.filter { it % 2 == 0 }
打印结果:
[2, 4, 6]
map函数
map简单理解就是映射的意思,即把一个元素映射(变换)成另一个元素,就像这样:
//每个元素变成它的平方
val mapList = numberList.map { it * it }
打印结果:
[1, 4, 9, 16, 25, 36]
map也可以过滤元素,只不过是讲元素过滤或者也可称之为变换成另一个元素,这里的过滤指的是过滤元素的属性.如下面这样:
val personList = listOf(Person("bob",21), Person("tina",21))
val mapPerson = personList.map { it.age }
打印结果:
[21, 21]
通过使用mapOf建立集合:
//通过mapOf函数建立集合
val mapOfList = mapOf(0 to "aaa",1 to "bbb")
//{0=aaa, 1=bbb}
通过mapValues映射集合的values
//通过mapValues映射集合的value
val mapValueList = mapOfList.mapValues { it.value.toUpperCase() }
//{0=AAA, 1=BBB}
此外,还有fileterKey,filterValue,mapKey也是同样的道理,用于过滤,变换key和value值
all函数
all用来判断所有元素是否满足某一条件,返回一个布尔值,例如判断集合中人的年龄是否大于20:
val all = personList.all { it.age > 20 }
打印结果:
true
any函数
any用来判断至少有一个元素满足某一条件,返回一个布尔值:
val any = personList.any { it.age > 21 }
打印结果:
false
count函数
count函数用来判断集合中满足条件的元素个数,当然你也可以先过滤元素,然后用.size方法来统计个数,但是这样会创建一个中间集合,而count函数只会用来跟踪元素的个数,而不关心元素本身,所有更加的高效
val count = personList.count { it.age > 19 }
打印结果:
2
find函数和firstOrNull函数
find用来发现集合中是否包含满足某一条件的元素,如果满足,返回第一个找到的元素,如果不满足则返回null.也可以用firstOrNull函数,作用是一样的,而且方法表现更明确
val find = personList.find { it.age == 21 }
println(find)
val findOrNull = personList.firstOrNull{ it.age > 21 }
println(findOrNull)
groupBy函数
groupBy函数可以用来对元素进行分组,可以把相同分组条件的元素分为同一组,然后结果是一个map,key是分组的条件值,value是符合这一条件的列表:
val personList1 = listOf(Person("bob",20), Person("tina",19))
val groupBy = personList1.groupBy { it.age }
打印结果:
{20=[Person(name=bob,age=20)], 19=[Person(name=tina,age=19)]}
flatMap函数
flatMap主要是实现两步操作,首先map也就是映射或者说变换成满足条件的集合,之后再flat也就是将所有元素平铺成一个集合:
val alist = listOf("abc","def","ghi")
var flatMap = alist.flatMap { it.toList() }
println(flatMap)
打印结果如下:
[a, b, c, d, e, f, g, h, i]
惰性操作集合Sequence
定义:Sequence是一个接口,表示是一个可以逐个列举元素的元素序列,它只有一个方法iterator,用来从序列中获取值.
优点: 避免创建中间集合,对数据量较大的集合做中间操作(过滤,变换)更高效.并且由于执行包含惰性,那末端操作未执行之前,所有的中间操作将不会执行.
所谓中间操作,末端操作:
image.png
示例如下所示:
val alist = listOf("abc","def","ghi")
val sequence = alist.asSequence().map { it.toUpperCase() }.filter { it == "ABC" }
sequence.toList()
在toList操作之前,map,filter操作都是延迟执行的,也就是说此事不会执行这些变换操作,只有在结果操作执行后才会执行.
另外一个和集合的区别,计算顺序:
对集合来说,比如下面这段代码:
alist.map { it.toUpperCase() }.filter { it == "ABC" }
先对全部的元素执行map操作,再对所有元素执行filter操作
对序列来说,下面代码:
alist.asSequence().map { it.toUpperCase() }.filter { it == "ABC" }
对每一个元素顺序运用map操作和filter操作,处理完一个元素,再去处理另一个元素.
例如如下代码操作:
val filterList1 = listOf(1, 2, 3, 4).map { it * it }.find { it > 3 }
执行过程如下:
1.对1执行map操作得到1,调用find函数判断是否大于3,结果是否
2.对2执行map操作得到4,调用find函数判断是否大于3,结果满足,返回,操作执行完毕,接下来的3,4不会再执行操作
流程图如下:
image.png
集合操作又被称为及早操作,序列操作相对而言又被称之为惰性操作.
创建序列,除了在集合上运用asSequence操作,还可以使用generateSequence函数,该函数生成序列并根据条件生成下一个元素.举一个小例子:
//算出0到100求和
generateSequence(0) { it + 1 }.takeWhile { it <= 100 }.sum()
这里同样注意下调用sum操作后之前的求值才会执行,也就是序列的惰性操作特性
lamabda使用,函数式接口
所谓函数式接口,如下所示:
public interface OnFocusChangeListener {
void onFocusChange(View v, boolean hasFocus);
}
像上面这样的只有一个方法的接口,称为函数式接口,也叫作单方法接口(SAM接口).在需要SAM接口作为参数时使用lamabda一般会更为方便也让代码看起来更简洁,符合习惯.
比如先定义下面的函数:
fun postPone(delay: Int,runnable: Runnable) {
println(delay)
runnable.run()
}
然后调用这个函数,先按常规方式,定义一个匿名内部类:
postPone(100, object: Runnable {
override fun run() {
println()
}
})
再按lamabda调用方式:
postPone(100, Runnable { println() })
比较两种方式:
1.调用方式上,第一种明显代码更多,而且语法更复杂,而第二种就简单的多
2.从性能上,此时第二种相比较第一种方式不会每次都创建runnable对象,如果lamabda表达式没有访问定义它的函数的变量,那么这个匿名对象不会每次都创建而是可以重用.
如果访问了包围它的作用域中捕捉了变量,那么这个变量会被保存,那么每次都会创建这个对象,此时就等价于第一种了,就像下面这样:
fun handId(id: Int) {
postPone(100, Runnable { println(id) })
}
此时每次调用都会创建对象.如果没有捕捉外部函数的变量,那么这个lamabda所代表的对象就是单例的,如果捕捉了变量,那么这个lamabda所代表的class文件会创建多个对象,并且类中会生成对应的字段用来保存这个值.lamabda底层会被编译成一个class文件.
上面的这段代码:
Runnable { println(id) }
这个用法叫做SAM的构造函数.SAM构造方法只接收一个参数,一个被用作方法体的lamabda表达式,并返回实现了这个接口的一个实例.
带接收者的lamabda
with函数
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}
接收两个参数:一个是接收者,一个是lamabda表达式,第一个参数会作为lamabda表达式的接受者,所谓接收者就是lamabda中作为this所指代的对象,在表达式中可以显示使用this,或者省略this直接调用接收者也就是第一个参数的方法
例如有下面这个例子,构建一串字符串:
fun buildString() : String {
val stringBuilder = StringBuilder()
stringBuilder.append("aaa")
stringBuilder.append("bbb")
stringBuilder.append("ccc")
return stringBuilder.toString()
}
现在用with函数改写这段代码:
fun buildStringWith() = with(StringBuilder()){
append("aaa")
append("bbb")
append("ccc")
toString()
}
其中返回值就是with函数中最后一行代码
如果要引用外部类的方法,例如toString(),如下所示:
//其中Outer代表外部类的类名Outer
this@Outer.toString()
apply函数
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
如果你要返回的不是lamabda的执行结果,而是接收者对象,那么这个时候需要用到apply函数,apply函数返回是这个接收者对象.如函数定义的最后一行返回的是this,同时,apply函数被声明成的是一个扩展函数.
使用场景:apply函数可以在任意对象上使用,它通常用来构建一个对象,就像java中的构造者模式一样,创建符合条件的对象.还有一些标准的库函数就是实现一些像上面一样的具体功能,例如buildString函数就是StringBuilder中一个已有的标准库函数.