Kotlin Note 是我学习kotlin
整理笔记向博客,文章中的例子参考了kotlin in action
这本书籍,同时我也极力推荐这本书,拥有Java
基础的人能够很快的就使用kotlin
来完善本身的编程技巧。html
不过我不想让博客变成简单的复制粘贴笔记,所以对内容进行了精简,同时增长了与Java
的对比和转换,一些详细内容不会整理出来,详细的内容我以为查阅api和翻书就能够了。java
博客中的例子须要一些简单的基础知识包括以下编程
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
等于java bean
的简化写法,你们都知道一个java bean
应当拥有get
set
toString
equals
hashCode
copy
等等方法与特性,data class
就是利用关键字data
在语言层面上构建了一个java bean
,若是有用过lombok
的小伙伴应该会熟悉(lombok
中在Java
类上使用注解@Data
在运行阶段生成以上说的一些方法来简化Java
开发)Java8
中的lambda语法仍是很简洁的,与kotlin
中的也十分类似,能够参考开始认识lambda,这里举简单的例子。c#
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表达式。api
kotlin
中的lambda表达式与Java
中的十分相似,相同的例子以下。app
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
集合,寻找到其中年龄最大的人并打印出来,下面看看不同的作法。函数
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
,而且使用效果很不错。
可这样命令式的代码仍是有点多了,而且这样的逻辑咱们可能会常常用到,所以将之概括整理成为类库再复用是更好的选择。学习
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中的排序
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 }
personList.maxBy{it.age }
不过值得注意的是,it
的这种写法虽然能够简化你的代码,可是会下降可读性,所以根据实际状况来考虑使用哪种。
函数式的编程风格在集合操做中有不少优点,大部分的操做均可以利用类库来完成,简化代码,提高效率。下面介绍一些基本经常使用的操做,事实上这些操做几乎存在在任何支持lambda表示的语言中,例如c#
scala
java8
等等,所以若是熟悉这些概念,简单的看看语法就ok了:)
这两个想必你们是十分熟悉了,过滤与映射,用法也与Java
中的用法十分相似,例子以下。
val list = listOf(1, 2, 3, 4, 5) println(list.filter { it % 2 == 1 }) // 选出全部的奇数 result : [1, 3, 5]
val list = listOf(1, 2, 3, 4, 5) println(list.map { it * 2 }) // 集合元素的值都翻倍 result : [2, 4, 6, 8, 10]
val list = listOf(1, 2, 3, 4, 5) println(list.all { it > 0 }) // result : true println(list.all { it > 1 }) // result : false
val list = listOf(1, 2, 3, 4, 5) println(list.any { it == 5 }) // result : true
val list = listOf(1, 2, 3, 4, 5) println(list.count { it >= 3 }) //result : 3
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能够将集合中的元素按照元素的某一个属性记性分类,相同的属性存在一个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
与前面提到的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
val lists = listOf(listOf(1, 2), listOf(3, 4)) println(lists.flatten()) //result : [1, 2, 3, 4]
上面的集合操做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))
persons.map(Person::name).filter { it.startsWith("j") }
上面的这段操做很简单,首先将person集合映射成了他们名字的字符串集合,接着过滤出名字以j
开头的字符串,经过翻看kotlin官方文档能够得知,上面这段操做会生成2个列表,一个用于保存filter
的结果,一个用于保存map
的结果。若是数据量很少的话并无什么问题,可若是数据量十分大的话,这样的操做调用就不合适了。
所以咱们须要将这样的操做变成了java8
中的stream
流式操做,在这里咱们使用asSequence
转为序列操做
persons.asSequence() .map(Person::name) .filter { it.startsWith("j") } .toList()
首先将集合转换为sequence
,接着进行一系列惰性求值操做,最后附加一条及早求值再转换为集合,这样的代码和java8
中的真的太相似了,下面贴一段java8
版本的。
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
这个词听起来很高端,也很不让人理解,其实简而言之就是,当你的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的匿名内部类的一种编译器的操做。
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
函数来简化这段代码
代码以下
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
函数与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) }
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了
这类带接收者的lambda是构建dsl的强力武器,在后面的部分我也会给出dsl的例子来构建属于本身的语言,其中就大量的利用到了这个特性。
本篇博客是一篇整理向博客,参考了kotlin in action
这本书的第五章节,同时将kotlin
中的lambda与java8
中的lambda进行了对比,能够发现二者之间的差异并非很大,所以做为一个熟悉java
语言的人是很能够很快的适应kotlin
的。本篇的核心知识点以下
kotlin
与java8
lambda语法的区别Sequence
与 Stream
的异同SAM
进行java
版本与kotlin
版本的lambda的无缝转换kotlin
中灵活多变的带接收者的函数若是你能阅读完本篇,但愿能激起你对kotlin
语言的一点兴趣 : )