java to kotlin (2) - lambda編程

前言

Kotlin Note 是我學習kotlin整理筆記向博客,文章中的例子參考了kotlin in action這本書籍,同時我也極力推薦這本書,擁有Java基礎的人能夠很快的就使用kotlin來完善本身的編程技巧。html

不過我不想讓博客變成簡單的複製粘貼筆記,所以對內容進行了精簡,同時增長了與Java的對比和轉換,一些詳細內容不會整理出來,詳細的內容我以爲查閱api和翻書就能夠了。java

基礎預備知識

博客中的例子須要一些簡單的基礎知識包括以下編程

Java8基礎

  • Java8中函數式編程的一些基本知識,例如lambda表達式 函數接口 Stream 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,這裏舉簡單的例子。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語法

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的類型能夠看出

演示

下面使用一段集合操做的例子來演示,咱們首先構建一個Personjava bean函數式編程

data class Person(val name: String, val age: Int)

很是簡潔,是吧?咱們的類Person擁有姓名和年齡,如今咱們有這麼一個需求,給定一個PersonList集合,尋找到其中年齡最大的人並打印出來,下面看看不同的作法。函數

命令式的操做 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中的流apilazy延遲操做的。lazy操做是函數編程中一個很常見也頗有用的操做,上文介紹的這些api並非lazy的,若是想轉換爲惰性的話,這時候Sequence就派上用場了。(ps : 關於惰性求職與及早求值能夠查看Java8函數之旅 (二) --Java8中的流外部迭代與內部迭代這一小段。
所以我以爲在這裏用Java8中的StreamSequence作類比是最合適不過的了。

下面的例子中使用這樣的一個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%啦 :)

區別與使用場景

既然sequencestream這麼相似,那麼應該怎麼選擇呢?

  • 若是你是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

withapply函數式最基本與最通用的附帶接受者的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的。本篇的核心知識點以下

  • kotlinjava8 lambda語法的區別
  • 一些經常使用的集合函數api
  • SequenceStream的異同
  • 利用SAM進行java版本與kotlin版本的lambda的無縫轉換
  • kotlin中靈活多變的帶接收者的函數

若是你能閱讀完本篇,但願能激起你對kotlin語言的一點興趣 : )

相關文章
相關標籤/搜索