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
語言的一點興趣 : )