java to kotlin (2) - lambda编程
前言
Kotlin Note 是我学习kotlin
整理笔记向博客,文章中的例子参考了kotlin in action
这本书籍,同时我也极力推荐这本书,拥有Java
基础的人可以很快的就使用kotlin
来完善自己的编程技巧。
不过我不想让博客变成简单的复制粘贴笔记,因此对内容进行了精简,同时增加了与Java
的对比和转换,一些详细内容不会整理出来,详细的内容我觉得查阅api和翻书就可以了。
基础预备知识
博客中的例子需要一些简单的基础知识包括如下
Java8基础
kotlin中的变量
kotlin
中变量的类型可以由编译器推导,只需要使用var
val
关键字来标注变量与不可变量即可,如果需要显示的标注,用冒号隔开写在变量后面。( 只要不加逗号都可以叫做一句话吧... 😃 )例如
var a = 5 //variable 可变量
val b = 10 //value 不可变量
var c : String = "Hello World" // 显示的指定String类型
var d = "Hello World" // 编译器会推导d的类型为String
data class
data class
等于java bean
的简化写法,大家都知道一个java bean
应当拥有get
set
toString
equals
hashCode
copy
等等方法与特性,data class
就是利用关键字data
在语言层面上构建了一个java bean
,如果有用过lombok
的小伙伴应该会熟悉(lombok
中在Java
类上使用注解@Data
在运行阶段生成以上说的一些方法来简化Java
开发)
lambda表达式 语法
Java中的lambda语法
Java8
中的lambda语法还是很简洁的,与kotlin
中的也十分相似,可以参考开始认识lambda,这里举简单的例子。
Consumer<String> out = (String s) -> {System.out.println(s)};//标准lambda表达式
Consumer<String> simpleOut = s -> System.out.println(s);//简化版
Consumer<String> methodRefOut = System.out::println; //方法引用版本
Java
中的lambda表达式本质上是匿名内部类,从上面的例子也可以看到我使用了Consumer
这个类来接收lambda表达式。
kotlin中的lambda语法
kotlin
中的lambda表达式与Java
中的十分类似,相同的例子如下。
val out = { x: String -> System.out.println(x) } //普通lambda表达式用{}包裹
val intOut: (String) -> Unit = { println(it) } //使用it来代替参数
val methodRefOut: (String) -> Unit = System.out::println //方法引用
- 在
kotlin
中lambda表达式使用{}
来包裹,其余与Java
基本相同 - 如果只有一个参数,
kotlin
中可以使用默认的it
来代替参数 - 第三行使用了方法引用,不同点是
kotlin
中的lambda表达式是函数而非匿名内部类,可以从(String)->Unit
的类型可以看出
演示
下面使用一段集合操作的例子来演示,我们首先构建一个Person
的java bean
data class Person(val name: String, val age: Int)
非常简洁,是吧?我们的类Person
拥有姓名和年龄,现在我们有这么一个需求,给定一个Person
的List
集合,寻找到其中年龄最大的人并打印出来,下面看看不一样的做法。
命令式的操作 in Java
public static void findOldestPerson(List<Person> personList) {
int maxAge = 0;
Person theOldest = null;
//遍历集合,如果发现有年龄更大的人就更新最大值
for (Person p : personList) {
if (p.getAge() > maxAge) {
maxAge = p.getAge();
theOldest = p;
}
}
System.out.println(theOldest);
}
public static void main(String[] args) {
List<Person> personList = Arrays.asList(
new Person("jack", 22),
new Person("rose", 25),
new Person("Tom", 19));
findOldestPerson(personList);
}
输出结果
Person(name=rose, age=25)
这里可以注意到我们在java
的代码中使用了kotlin
中定义的data class
,并且使用效果很不错。
可这样命令式的代码还是有点多了,并且这样的逻辑我们可能会经常用到,因此将之归纳整理成为类库再复用是更好的选择。
使用Java8的stream api
import static java.util.Comparator.*;
List<Person> personList = Arrays.asList(
new Person("jack", 22),
new Person("rose", 25),
new Person("Tom", 19));
Person theOledest = personList.stream()
.max(comparingInt(Person::getAge))
.get();
System.out.println(theOledest);
这里使用了max方法,传入了一个比较器,详细知识可以查看Java8函数之旅 (五) -- Java8中的排序
使用kotlin的lambda
val personList = listOf(Person("jack", 22), Person("rose", 25))
println(personList.maxBy{p:Person -> p.age })//标准写法
println(personList.maxBy{it.age})//使用it来简化参数
println(personList.maxBy(Person::age)) //方法引用
上面使用了几种不同的写法来表述这个操作,这里选出标准写法
personList.maxBy({p:Person -> p.age })
这段代码的可读性还是很好的,花括号中是lambda表达式,表示对于maxBy函数来说,要比较的是Person的年龄。但语法上还是有一些啰嗦,这里一步一步的进行简化
- 过多的标点符号破坏了可读性
对于这一点,kotlin
中约定如果lambda表达式是函数调用的最后一个参数,参数可以放到括号外面
personList.maxBy(){p:Person -> p.age }
并且当lambda表达式是函数的唯一一个参数时,可以去掉空括号
personList.maxBy{p:Person -> p.age }
- 类型可以从上下文推断,不需要显示的指明
personList.maxBy{p -> p.age }
- 当只有一个参数时,可以用it的默认名称
personList.maxBy{it.age }
不过值得注意的是,it
的这种写法虽然可以简化你的代码,但是会降低可读性,因此根据实际情况来考虑使用哪一种。
常见的集合函数api
函数式的编程风格在集合操作中有很多优势,大部分的操作都可以利用类库来完成,简化代码,提升效率。下面介绍一些基本常用的操作,事实上这些操作几乎存在在任何支持lambda表示的语言中,例如c#
scala
java8
等等,因此如果熟悉这些概念,简单的看看语法就ok了:)
基础操作 filter , map
这两个想必大家是十分熟悉了,过滤与映射,用法也与Java
中的用法十分类似,例子如下。
- filter 过滤
val list = listOf(1, 2, 3, 4, 5)
println(list.filter { it % 2 == 1 })
// 选出所有的奇数 result : [1, 3, 5]
- map 映射
val list = listOf(1, 2, 3, 4, 5)
println(list.map { it * 2 })
// 集合元素的值都翻倍 result : [2, 4, 6, 8, 10]
对集合进行判断 all,any,count,find
- all 检查所有元素是否满足条件
val list = listOf(1, 2, 3, 4, 5)
println(list.all { it > 0 }) // result : true
println(list.all { it > 1 }) // result : false
- any 检查是否有任意一个元素满足条件
val list = listOf(1, 2, 3, 4, 5)
println(list.any { it == 5 }) // result : true
- count 获得满足条件的元素的个数
val list = listOf(1, 2, 3, 4, 5)
println(list.count { it >= 3 }) //result : 3
- find 获得第一个满足条件的元素
如果找到了就返回第一个满足条件的元素,如果没找到就返回null,因此find
拥有一个同义的apifirstOrNull
val list = listOf(1, 2, 3, 4, 5)
println(list.find { it >= 1 }) //result : 1
println(list.firstOrNull { it >= 10 }) //result : null
groupBy 将List分组为map
groupBy可以将集合中的元素按照元素的某一个属性记性分类,相同的属性存在一个key中,例子如下
val persons = listOf(Person("jack", 22),
Person("jack", 28),
Person("rose", 25))
persons.groupBy { it.name }
.forEach{key, value -> println("key : $key -> value : $value") }
// result :
//key : jack -> value : [Person(name=jack, age=22), Person(name=jack, age=28)]
//key : rose -> value : [Person(name=rose, age=25)]
可以看到生成的map是按照Person的姓名进行分组
flatMap , flatten
- flatMap 映射然后平铺
flatMap
与前面提到的map
操作很像,区别在于map
只有一个操作,那就是映射。而flatMap
是在映射完之后,进行了合并(平铺)的操作,例子如下
val lists = listOf(listOf(1, 2), listOf(3, 4))
println(lists.map { it.map { it * 2 } })
// result : [[2, 4], [6, 8]]
println(lists.flatMap { it.map { it * 2 } })
// result : [2, 4, 6, 8]
上面的例子有一点绕口,lists里面包含里2个集合也就是[[1,2],[3,4]]
,使用map只能将里面的元素给映射,却不能将这2个集合给整合(平铺)成一个集合,而flatMap
就可以做到,相信通过结果这其中的区别应该很容易发现
- flatten 平铺
如果你只想平铺不想映射,就可以使用flatten
val lists = listOf(listOf(1, 2), listOf(3, 4))
println(lists.flatten())
//result : [1, 2, 3, 4]
Sequence 与 Stream
上面的集合操作api很容易联想到Java8
中的stream流 api
,可事实这两者并不完全一样,Java8
中的流api
是lazy
延迟操作的。lazy
操作是函数编程中一个很常见也很有用的操作,上文介绍的这些api并不是lazy
的,如果想转换为惰性的话,这时候Sequence
就派上用场了。(ps : 关于惰性求职与及早求值可以查看Java8函数之旅 (二) --Java8中的流外部迭代与内部迭代
这一小段。
因此我觉得在这里用Java8
中的Stream
与Sequence
做类比是最合适不过的了。
下面的例子中使用这样的一个peron 集合来做操作
val persons = listOf(Person("jack", 22), Person("rose", 25))
normal filter,map
persons.map(Person::name).filter { it.startsWith("j") }
上面的这段操作很简单,首先将person集合映射成了他们名字的字符串集合,接着过滤出名字以j
开头的字符串,通过翻看kotlin官方文档可以得知,上面这段操作会生成2个列表,一个用于保存filter
的结果,一个用于保存map
的结果。如果数据量不多的话并没有什么问题,可如果数据量十分大的话,这样的操作调用就不合适了。
因此我们需要将这样的操作变成了java8
中的stream
流式操作,在这里我们使用asSequence
转为序列操作
sequence filter,map
persons.asSequence()
.map(Person::name)
.filter { it.startsWith("j") }
.toList()
首先将集合转换为sequence
,接着进行一系列惰性求值操作,最后附加一条及早求值再转换为集合,这样的代码和java8
中的真的太类似了,下面贴一段java8
版本的。
stream filter,map
persons.stream()
.map(Person::getName)
.filter(name -> name.startsWith("j"))
.collect(toList());
相似度高达99% ! 其实也没有99%啦 😃
区别与使用场景
既然sequence
与stream
这么类似,那么应该怎么选择呢?
-
如果你是
Java
的老版本也想体验一下函数式编程与流式操作的快感,那么毫无疑问你只有sequence
选择啦(stream
是基于Java8
的,而kotlin
是基于Java6
的) -
Java8
中流的过人之处在于提供了十分方面的并行流,只需要使用parallelStream()
即可使用多核CPU来计算啦~~ 而这一点sequence
中并没有提供
因此究竟怎么选择,还是要看你的Java
版本和实际需求
SAM
解释
SAM
这个词听起来很高端,也很不让人理解,其实简而言之就是,当你的kotlin
的代码在调用java
的一些函数接口的时候,可以无缝转换(这一点其实编程者不会明显的感觉到,因为是编译器在作用)
SAM
的全称Single Abstract Method Conversions
,翻译过来单抽象方法(接口)转换,那大家都清楚,在Java8
中,如果你的接口只有一个抽象方法(未实现的方法),那么这样的接口就称之为函数式接口
,换言之,这样的接口作为参数时,你可以直接传递lambda表达式
。
例如在Java
中
new Thread(() -> System.out.println(123))
正是因为thread
的参数时一个实现runnable
接口的类,而runnable
接口的源码如下
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
无论是从注解和方法签名都可以断定,这就是一个函数式接口,所以当kotlin在调用这类api的时候,编译器会100%的编译成Java
版本的字节码以达到无缝转换的作用。再说的直白点就是,kotlin
中的lambda
表达式不是匿名内部类,但Java8
中的函数接口却是的,因此在Java8
中存在一些方法(例如上面提到的thread)会接受这些看起来像lambda参数而实际上匿名内部类的函数接口,而当kotlin
调用这些方法的时候,编译器就会将kotlin
的纯正lambda转化为匿名内部类以达到适配的效果。
验证
例子如下
val number = 5
Thread{ println(number) }
这是一段kotlin构建线程的代码,使用kotlin的字节码工具查看字节码
LINENUMBER 8 L2
NEW java/lang/Thread
DUP
NEW BlogKt$main$1
DUP
ILOAD 1
INVOKESPECIAL BlogKt$main$1.<init> (I)V
CHECKCAST java/lang/Runnable
INVOKESPECIAL java/lang/Thread.<init> (Ljava/lang/Runnable;)V
INVOKEVIRTUAL java/lang/Thread.start ()V
L3
可以看到
CHECKCAST java/lang/Runnable
INVOKESPECIAL java/lang/Thread.<init> (Ljava/lang/Runnable;)V
很明显的生成转换了Runnable
的实例,我们再将字节码反编译成Java
代码
final int number = 5;
(new Thread((Runnable)(new Runnable() {
public final void run() {
int var1 = number;
System.out.println(var1);
}
}))).start();
验证了这一理论,字节码的checkcast就是强转的(Runnable),下面一行就是生成Runnable
实例
一句话总结
SAM
就是kotlin
在调用Java
的函数式接口的时候,能够准确的将kotlin
中的lambda表达式转化为对应的Java的匿名内部类的一种编译器的操作。
带接收者的lambda表达式
kotlin
中有很多扩展性很高并且很有趣的函数,这些函数可以简化你的代码,同时也是强大的DSL
的基础。这里介绍2个,一个是with
函数,一个是applay
函数。
例子
现在我们要构建一个字母表的函数,初始代码如下
fun alphabet(): String {
var result = StringBuilder()
for (letter in 'A'..'Z') {
result.append(letter)
}
result.append("\n字母表构建好了")
return result.toString()
}
fun main(args: Array<String>) {
println(alphabet())
}
// 输出结果
>>> ABCDEFGHIJKLMNOPQRSTUVWXYZ
>>> 字母表构建好了
通过观察可以发现函数alphabet
中调用了很多次result实例的方法,因此result这个词语反复的在出现,这时候我们就可以通过with
函数来简化这段代码
with 函数
代码如下
fun alphabet(): String {
var sb = StringBuilder()
return with(sb) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\n字母表构建好了")
this.toString()
}
}
语法with(sb){ }
看起来感觉像是一种新的语法结构,其实并不是,这只是函数调用,我们观察一下with
函数的函数签名
public inline fun <T, R> with(receiver: T, block: T.() -> R): R
发现最后一个参数是lambda表达式,那么根据之前介绍kotlin
中lambda表达式的第二条,如果一个函数的最后一个参数是一个lambda表达式,那么可以将参数的花括号移到外面。这样一解释是不是就清楚了许多?sb是with的第一个参数,而后面花括号的就是第二个参数,也就是一段lambda表达式。
这个方法签名值得让人注意的是这一段with(receiver: T, block: T.() -> R)
T.()
的意思是第二段的lambda表达式的默认参数就是前面的receiver
,因此上面的代码with(sb)
后面的这一段lambda表达式中默认方法的调用者都是stringbuilder
我们再对上面的代码做一点改进
fun alphabet(): String {
return with(StringBuilder()) {
for (letter in 'A'..'Z') {
append(letter)
}
append("\n字母表构建好了")
toString()
}
}
第一个参数直接将构造函数结果赋予进去,最后一行省略this
apply 函数
apply
函数与with
函数十分类似,区别在于with
的返回值是lambda表达式的返回值,也就是lambda表达式的最后一行,而apply
的返回值是调用者本身,观察方法签名也可以得出这个结论。
public inline fun <T> T.apply(block: T.() -> Unit): T
下面我们用applay
函数来写上面这个例子
fun alphabet(): String {
return StringBuilder().apply {
for (letter in 'A'..'Z') {
append(letter)
}
append("\n字母表构建好了")
}.toString() //区别就在于返回值,这里在外面调用toString
}
在这里apply
接受stringBuilder
然后lambda表达式里默认参数就是stringBuilder
最后返回值也是stringBuilder
apply
在很多时候都很有用,例如其中一个场景就是在初始化一些属性的时候,例如在安卓中初始化一个textView
fun createViewWithCustomAttributes(context: Context) =
TextView(context).apply{
text = "Sample Text"
textSize = 20.0
setPadding(10, 0, 0, 0)
}
使用buildString
with
与apply
函数式最基本与最通用的附带接受者的lambda函数,事实上很多类库中对这些基础函数进行了封装因此会出现很多很好用的封装之后的接收者函数,这类函数众多,没法一一将来,也没什么必要,大家可以自行查阅api以及相关资料: ) 这里我们还是用上面的例子来讲解,使用buildString
来构造
先看看它的签名与代码
public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
StringBuilder().apply(builderAction).toString()
观察签名发现这个函数就是为你省去了创建stringBuilder
与结尾的toString
操作,这下子就容易理解了,使用buildString
构建字母表的代码如下
fun alphabet(): String {
return buildString {
for (letter in 'A'..'Z') {
append(letter)
}
append("\n字母表构建好了")
}
}
默认的为你提供了stringBuilder
以及结束时的toString
,你只需要负责构建逻辑就OK了
DSL的强力构建武器
这类带接收者的lambda是构建dsl的强力武器,在后面的部分我也会给出dsl的例子来构建属于自己的语言,其中就大量的利用到了这个特性。
总结
本篇博客是一篇整理向博客,参考了kotlin in action
这本书的第五章节,同时将kotlin
中的lambda与java8
中的lambda进行了对比,可以发现两者之间的差别并不是很大,因此作为一个熟悉java
语言的人是很可以很快的适应kotlin
的。本篇的核心知识点如下
kotlin
与java8
lambda语法的区别- 一些常用的集合函数api
Sequence
与Stream
的异同- 利用
SAM
进行java
版本与kotlin
版本的lambda的无缝转换 kotlin
中灵活多变的带接收者的函数
如果你能阅读完本篇,希望能激起你对kotlin
语言的一点兴趣 : )