Kotlin教程(六)Lambda編程

寫在開頭:本人打算開始寫一個Kotlin系列的教程,一是使本身記憶和理解的更加深入,二是能夠分享給一樣想學習Kotlin的同窗。系列文章的知識點會以《Kotlin實戰》這本書中順序編寫,在將書中知識點展現出來同時,我也會添加對應的Java代碼用於對比學習和更好的理解。express

Kotlin教程(一)基礎
Kotlin教程(二)函數
Kotlin教程(三)類、對象和接口
Kotlin教程(四)可空性
Kotlin教程(五)類型
Kotlin教程(六)Lambda編程
Kotlin教程(七)運算符重載及其餘約定
Kotlin教程(八)高階函數
Kotlin教程(九)泛型編程


Lambda表達式,或簡稱lambda,本質上級就是能夠傳遞給其餘函數的一小段代碼。有了lambda,能夠輕鬆地把通用的代碼結構抽取成庫函數,Kotlin標準庫就大量地使用了它們。bash

Lambda表達式和成員引用

把lambda引入Java 8是Java這門語言演變過程當中讓人望眼欲穿的變化之一。爲何它是如此重要?這一節中,你會發現爲什麼lambda這麼好用,以及Kotlin的lambda語法看起來是什麼樣子的。app

Lambda簡介:做爲函數參數的代碼塊

在你代碼中存儲和傳遞一小段行爲是常有的任務。例如,你經常須要表達像這樣的想法:「當一個時間發生的時候運行這個事件處理器」又或者是「把這個操做應用到這個數據接口中全部元素上」。在老版本的Java中,可使用匿名內部類來實現。這種技巧能夠工做可是語法太囉嗦了。
函數式編程提供了另一種解決問題的方法:把函數當作值來對待。能夠直接傳遞函數,而不須要先聲明一個類再傳遞這個類的實例。使用lambda表達式以後,代碼會更加簡潔。都不須要聲明函數了,能夠高效地直接傳遞代碼塊做爲函數參數。
咱們來看一個例子。假設你要定義一個點擊按鈕的行爲,添加一個負責處理點擊的監聽器。監聽器實現了相應的接口OnClickListener和它的一個方法onClick:異步

/* Java */
button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //do something
        }
});
複製代碼

這樣聲明匿名內部類的寫法實在是太囉嗦了。在Kotlin中咱們能夠像Java 8同樣使用lambda來消除這些冗餘代碼。ide

/* Kotlin */
button.setOnClickListener{ /* do someting */ }
複製代碼

這段代碼作了與上面一樣的事情,可是不用再寫囉嗦的匿名內部類了。
以前也說過Kotlin可使用關鍵字object 匿名內部類,所以,你想寫成普通的方式也是能夠的:函數式編程

button.setOnClickListener(object : View.OnClickListener {
            override fun onClick(v: View?) {
                println("on click")
            }
        })
複製代碼

上面兩種方式轉換成Java代碼:函數

button.setOnClickListener((OnClickListener)null.INSTANCE);
button.setOnClickListener((OnClickListener)(new OnClickListener() {
     public void onClick(@Nullable View v) {
        String var2 = "on click";
        System.out.println(var2);
     }
  }));
複製代碼

匿名內部類轉換成了Java的匿名內部類。可是lambda應該是Kotlin本身作了特出處理,沒法轉換成相應的Java代碼。post

Lambda和集合

咱們先來看一個例子,你會用到一個Person類,它包含這我的的名字和年齡信息:性能

data class Person(val name: String, val age: Int)
複製代碼

假設如今你有一我的的列表,須要找到列表中年齡最大的那我的。若是徹底不瞭解lambda,你可能會這樣作:

fun findTheOldest(people: List<Person>) {
    var maxAge = 0
    var theOldest: Person? = null
    for (person in people) {
        if (person.age > maxAge) {
            maxAge = person.age
            theOldest = person
        }
    }
    println(theOldest)
}

>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> findTheOldest(people)
Person("Alice", 29)
複製代碼

能夠完成目的,可是代碼稍微有點多。而Kotlin有更好的方法,可使用庫函數:

>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> println(people.maxBy{ it.age })
Person("Alice", 29)
複製代碼

maxBy函數能夠在任何集合上調用,且只須要一個實參:一個函數,指定比較哪一個值來找到最大元素。花括號中的代碼{ it.age } 就是實現了這個邏輯的lambda。它接收一個集合中的元素做爲實參(使用it引用它)而且返回用來比較的值。這個例子中,集合元素是Person對象,用來比較的是存儲在其age屬性中的年齡。
若是lambda恰好是函數或者屬性的委託,能夠用成員引用替換:

people.maxBy{ Person::age }
複製代碼

雖然lambda看上去很簡潔,可是你可能不是很明白究竟是如何寫lambda,以及裏面的規則,咱們來學習下lambda表達式的語法吧。

Lambda表達式的語法

一個lambda把一小段行爲進行編碼,你能把它當作值處處傳遞。它能夠被獨立地聲明並存儲到一個變量中。可是更常見的仍是直接聲明它並傳遞給函數。

//參數           //函數體
{ x: Int, y: Int -> x + y }
複製代碼

Kotlin的lambda表達式始終用花括號包圍。->把實參和函數體分割開,左邊是參數列表,右邊是函數體。注意參數並無用() 括起來。
能夠把lambda表達式存儲在一個變量中,把這個變量當作普通函數對待(即經過相應實參調用它):

>>> val sum = {x:Int,y:Int -> x + y}
>>> println(sum(1, 2))
3
複製代碼

若是你樂意,還能夠直接調用lambda表達式:

>>> { println(42) }()
42
複製代碼

可是這樣的語法毫無可讀性,也沒有什麼意義(它等價於直接執行lambda函數體中的代碼)。若是你確實須要把一小段代碼封閉在一個代碼塊中,可使用庫函數run來執行傳遞它的lambda:

>>> run{ println(42) }
42
複製代碼

在以後的章節咱們會了解到這種調用和內建語言結構同樣高效且不會帶來額外運行時開銷,以及背後的緣由。如今咱們繼續看「找到列表中年齡最大」的例子:

>>> val people = listOf(Person("Alice", 29), Person("Hubert", 26))
>>> println(people.maxBy{ it.age })
Person("Alice", 29)
複製代碼

若是不用任何簡明語法來重寫這個例子,你會獲得下面的代碼:

people.maxBy({ p: Person -> p.age })
複製代碼

這段代碼一目瞭然:花括號中的代碼片斷是lambda表達式,把它做爲實參傳給函數。這個lambda接收一個類型爲Person的參數並返回它的年齡。
可是這段代碼有點囉嗦。首先,過多的標點符號破壞了可讀性。其次,類型能夠從上下文推斷出來並能夠省略。最後,這種狀況下不須要給lambda的參數分配一個名稱。
讓咱們來改進這些地方,先拿花括號開刀。Kotlin有這樣一種語法約定,若是lambda表達式是函數調用的最後一個實參,它能夠放到括號的外邊。這個例子中,lambda是惟一的實參,因此能夠放到括號的後邊:

people.maxBy() { p:Person -> p.age }
複製代碼

當lambda時函數惟一的實參時,還能夠去掉調用代碼中的空括號:

people.maxBy { p:Person -> p.age }
複製代碼

三種語法形式含義都是同樣的,但最後一種最易讀。若是lambda是惟一的實參,你固然願意在寫代碼的時候省掉這些括號。而當你有多個實參時,便可以把lambda留在括號內來強調它是一個實參,也能夠把它放在括號的外面,兩種選擇都是可行的。若是你想傳遞兩個更多的lambda,不能把超過一個lambda放在外面。
咱們來看看這些選項在更復雜的調用中是怎樣的。還記得外面在教程二中定義的joinToString函數嗎?Kotlin標準庫中也有定義它,不一樣之處在於它能夠接收一個附加的函數參數。這個函數能夠用toString函數之外的方法來把一個元素轉換成字符串。下面的例子展現了你能夠用它只打印出人的名字:

>>> val names = people.joinToString(separator = " ", transform = { p: Person -> p.name })
>>> println(names)
Alice Hubert
複製代碼

這種方式使用命名實參來傳遞lambda,清楚地表示了lambda應用到了哪裏。
下面的例子展現課能夠怎樣重寫這個調用,把lambda放在括號外:

>>> val names = people.joinToString(" ") { p: Person -> p.name }
>>> println(names)
Alice Hubert
複製代碼

這種方式沒有顯式地代表lambda引用到了哪裏,因此不熟悉被調用函數的那些人可能更難理解。

在as或者IDEA中可使用Alt+Enter喚起操做,使用「Move lambda expression out of parentheses 」把lambda表達式移動到括號外,或「Move lambda expression into parentheses」把lambda表達式移動到括號內。

咱們繼續簡化語法,移除參數的類型。

people.maxBy { p:Person -> p.age }
people.maxBy { p -> p.age }  //推導出參數類型
複製代碼

和局部變量同樣,若是lambda參數的類型能夠被推導出來,你就不須要顯示地指定它。以這裏的maxBy函數爲例,其參數類型始終和集合的元素類型相同。編譯器知道你是對一個Person對象的集合調用maxBy函數,因此它能推導lambda參數也會是Person類型。
也存在編譯器不能推斷出lambda參數類型的狀況,但這裏咱們暫不討論。能夠遵循這樣的一條簡單的規則:先不聲明類型,等編譯器報錯後再來指定它們。
這個例子你能作的最後簡化是使用默認參數名稱it代替命名參數。若是當前上下文指望的是隻有一個參數的lambda且這個參數的類型能夠推斷出來,就會生成這個名稱。

people.maxBy { it.age }  //it是自動生成的參數名稱
複製代碼

僅實參名稱沒有顯示地指定時這個默認的名稱纔會生成。

it約定能大大縮短你的代碼,但你不該該濫用它。尤爲是在嵌套lambda的狀況下。最好顯式地聲明每一個lambda的參數。fouz,很難搞清楚it引用的究竟是那個值。若是上下文中參數的類型或意義都不是很明朗,顯式聲明參數的方法也頗有效。

若是你用變量存儲lambda,那麼就沒有能夠推斷出參數類型的上下文,因此你必須顯式地指定參數類型:

>>> val getAge = { p:Person -> p.age }
>>> people.maxBy(getAge)
複製代碼

至此你看到的例子都是單個表達式或語句構成的lambda。可是lambda並無 被限制在這樣小的規模,它能夠包含更多的語句。下面這種狀況,最後一個表達式就是(lambda的)結果:

val sum = { x: Int, y: Int ->
        println("Computing the sum of $x and $y ...")
        x + y
    }
>>> println(sum(1, 2))
Computing the sum of 1 and 2 ...
3
複製代碼

在做用域中訪問變量

當在函數內聲明一個匿名內部類的時候,可以在這個匿名內部類引用這個函數的參數和局部變量。也能夠用lambda做一樣的事情。若是在函數內部使用lambda,也能夠訪問這個函數的參數,還有在lambda以前定義的局部變量。
咱們用標準庫函數forEach來展現這種行爲。這個函數可以遍歷集合中的每個元素,並在該元素上調用給定的lambda。forEach函數只是比普通for循環更簡潔一些。

fun printMessageWithPrefix(message: Collection<String>, prefix: String) {
    //接受lambda做爲實參指定對每一個元素的操做
    message.forEach {
        println("$prefix $it")  //在lambda中訪問prefix參數
    }
}

>>> val errors = listOf("403 Forbidden", "404 Not Found")
>>> printMessageWithPrefix(errors, "Error:")
Error: 403 Forbidden
Error: 404 Not Found
複製代碼

這裏Kotlin和Java的一個顯著區別是:在Kotlin中不會僅限於訪問final變量,在lambda內部也能夠修改這些變量:

fun printProblemCounts(response: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    response.forEach {
        if (it.startsWith("4")) {
            clientErrors++
        } else if (it.startsWith("5")) {
            serverErrors++
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}

>>> val response = listOf("200 OK", "418 I'm a teapot", "500 Internal Server Error")
>>> printProblemCounts(response)
1
複製代碼

和Java不同,Kotlin容許在lambda內部訪問非final變量甚至修改它們。從lambda內訪問外部變量,咱們稱這些變量被lambda捕獲,就像這個例子中的prefix、clientErrors以及serverErrors同樣。

訪問非final變量甚至修改它們的原理

注意,默認狀況下,局部變量的生命週期被限制在聲明這個變量的函數中,可是若是它被lambda捕獲了,使用這個變量的代碼能夠被存儲並稍後再執行。你可能會問這事什麼原理?當你捕獲final變量時,它的值和使用這個值的lambda代碼一塊兒存儲。而對非final變量來講,它的值被封裝在一個特殊的包裝器中,這樣你就能夠改變這個值,而對這個包裝器的引用會和lambda代碼一塊兒存儲。
這個原理我在教程三中的匿名內部類中也有提到:訪問建立匿名內部類的函數中的變量是沒有限制在final變量,當時舉了這個例子:

var clickCount = 0 
B().setListener(object : Listener {
    override fun onClick() {
        clickCount++ //修改變量
    }
})
複製代碼

而且轉換成了Java代碼:

final IntRef clickCount = new IntRef();
clickCount.element = 0;
(new B()).setListener((Listener)(new Listener() {
   public void onClick() {
      int var1 = clickCount.element++;
   }
}));
複製代碼

能夠看到真實被使用clickCount是int類型數,但在Java中使用確實包裝類IntRef,而真實int變成了clickCount.element。
任什麼時候候你捕獲了一個final變量(val),它的值被拷貝下來,這和Java同樣。而當你捕獲了一個可變變量(var)時,它的值被做爲Ref類的一個實例被存儲下來。Ref變量是final的能輕易被捕獲,然而實際值被存儲在其字段中,而且能夠在lambda內修改。

這裏有個重要的注意事項,若是lambda被用做事件處理器或者用在其餘異步執行的狀況,對局部變量的修改只會在lambda執行的時候發生。例以下面這段代碼並非記錄按鈕點擊次數的正確方法:

fun tryToCountButtonOnClicks(button: Button): Int {
    var clicks = 0
    button.setOnClickListener { clicks++ }
    return clicks
}
複製代碼

這個函數始終返回0。儘管onClick處理器能夠修改clicks的值,你並不能觀察到值發生了變化,由於onClick處理器是在函數返回以後調用的。這個函數正確的實現須要把點擊次數存儲在函數外依然能夠訪問的地方——例如類的屬性,而不是存儲在函數的局部變量中。

成員引用

你已經看到lambda是如何讓你把代碼塊做爲參數傳遞給函數的。可是若是你想要當作參數傳遞的代碼已經被定義成了函數,該怎麼辦?固然能夠傳遞一個調用這個函數的lambda,但這樣作有點多餘。name你能直接傳遞函數嗎?
Kotlin和Java 8同樣,若是把函數轉換成一個值,你就能夠傳遞它。使用:: 運算符來轉換:

val getAge = Person::age
複製代碼

這種表達式稱爲成員引用,它提供了簡明語法,來建立一個調用單個方法或者訪問單個屬性的函數值。雙冒號把類名稱與你要引用的成員(一個方法或者一個屬性)名稱隔開。
一樣的內容用lambda表達式實現是這樣的:

val getAge = { person: Person -> person.age }
複製代碼

無論你引用的函數仍是屬性,都不要在成員引用的名稱後面加括號。成員引用和調用該函數的lambda具備同樣的類型,因此能夠互換使用。

還能夠引用頂層函數(不是類的成員):

fun salute() = println("Salute!")
>>> run(::salute)
Salute!
複製代碼

這種狀況下,你省略了類名稱,直接以:: 開頭。成員引用::salute 被當作實參傳遞庫函數run,它會調用相應的函數。
若是lambda要委託給一個接收多個參數的函數,提供成員引用代替它將會很是方便:

val action = { person: Person, message: String ->
    sendEmail(person, message)  //這個lambda委託給sendEmail函數
}
val nextAction = ::sendEmail  //能夠用成員引用代替
複製代碼

能夠用構造方法引用存儲或者延期執行建立類實例的做用。構造方法引用的形式是在雙冒號後指定類名稱:

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

>>> val createPerson = ::Person
>>> val p = createPerson("Hubert", 26)  //建立Person實例的動做被保存成了值
>>> println(p)
Person(name=Hubert, age=26)
複製代碼

還能夠用一樣的方式引用擴展函數:

fun Person.isAdult() = age >= 18
val predicate = Person::isAdult
複製代碼

儘管isAdult不是Person類的成員,仍是能夠經過引用訪問它,這和訪問實例的成員沒什麼兩樣:person.isAdult()

綁定引用

在Kotlin 1.0 中,當接受一個類的方法或屬性引用時,你始終須要提供一個該類的實例來調用這個引用。Kotlin 1.1 計劃支持綁定成員引用,它容許你使用成員引用語法捕獲特定實例對象上的方法引用。

>>> val p = Person("Hubert", 26)
>>> val personsAgeFunction = Person::age
>>> println(personsAgeFunction(p))
26
>>> val hubertsAgeFunction = p::age  //Kotlin 1.1 中可使用綁定成員引用
>>> println(hubertsAgeFunction())
26
複製代碼

注意personsAgeFunction是一個單參數函數(返回給定人的年齡),而hubertsAgeFunction是一個沒有參數的函數(返回p對象的年齡)。在Kotlin 1.1 以前,你須要顯式地寫出lambda{ p.age } ,而不是使用綁定成員引用p::age

集合的函數式API

函數式編程風格在操做集合時提供了不少優點。大多數任務均可以經過庫函數完成,來簡化你的代碼。

filter 和 map

filter和map函數造成了集合操做的基礎,不少集合操做都是藉助它們來表達的。
每一個函數咱們都會給出兩個例子,一個使用數字,另外一個使用熟悉的Person類:

data class Person(val name: String, val age: Int)
複製代碼

filter函數遍歷集合並選出應用給定lambda後會返回true的那些元素:

>>> val list = listOf(1, 2, 3, 4)
>>> println(list.filter {  it % 2 == 0 })  //只留下偶數
[2, 4]
複製代碼

上面的結果是一個新的集合,它只包含輸入集合中那些知足判斷是的元素。
若是你想留下那些超過30歲的人,能夠用filter:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.filter { it.age > 30 })
Person(name=Bob, age=31)
複製代碼

filter函數能夠從集合中移除你不想要的元素,可是它並不會改變這些元素。元素的變換是map的用武之地。
map函數對集合中的每個元素應用給定的函數並把結果收集到一個新集合。能夠把數字列表變換成它們平方的列表,好比:

>>> val list = listOf(1, 2, 3, 4)
>>> println(list.map { it * it })
[1, 4, 9, 16]
複製代碼

結果是一個新集合,包含的元素個數不變,可是每一個元素根據給定的判斷式作了變換。
若是你想打印的只是一個姓名的列表,而不是人的完整信息列表,能夠用map來變換列表:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.map { it.name })
[Hubert, Bob]
複製代碼

這個例子也能夠同成員引用漂亮地重寫:

people.map(Person::name)
複製代碼

能夠輕鬆地把屢次這樣的調用連接起來。例如,打印出年齡超過30歲的人的名字:

>>> people.filter { it.age > 30 }.map(Person::name)
[Bob]
複製代碼

如今,若是說須要這個分組中全部年齡最大的人的名字,能夠先找到分組中人的最大年齡,而後返回全部這個年齡的人,很容易就用lambda寫出以下代碼:

people.filter { it.age == people.maxBy(Person::age).age }
複製代碼

可是注意,這段代碼對每一個人都會重複尋找最大年齡的過程,假設集合中有100我的,尋找最大年齡的過程就會執行100遍!下面的解決方法作出了改進,只計算了一次最大年齡:

val maxAge = people.maxBy(Person::age).age
people.filter { it.age == maxAge }
複製代碼

若是沒有必要就不要重複計算!使用lambda表達式的代碼看起來簡單,有時候卻掩蓋底層操做的複雜性。始終牢記你寫的代碼在幹什麼。
還能夠對map集合應用過濾和變換函數:

>>> val numbers = mapOf(0 to "zero", 1 to "one")
>>> println(numbers.mapValues { it.value.toUpperCase() })
{0=ZERO, 1=ONE}
複製代碼

鍵和值分別由各自的函數來處理。filterKeys和mapKeys過濾和變換map集合的鍵,而另外的filterValues和mapValues過濾和變換對應的值。

"all" "any" "count"和"find":對集合應用判斷

另外一種常見的任務是檢查集合中全部元素是否都符合某個條件(或者它的變種,是否存在符合的元素)。Kotlin中,它們是經過all和any函數表達的。count函數檢查有多少元素知足判斷式,而find函數返回第一個符合條件的元素。
爲了演示這些函數,咱們先來定義一個判斷式,來檢查一我的是否尚未到28歲:

val canBeInClub27 = { p:Person -> p.age <= 27 }
複製代碼

若是你對是否全部元素都知足判斷式感興趣,應該使用all函數:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.all(canBeInClub27))
false
複製代碼

若是你須要檢查集合中是否至少存在一個匹配的元素,那就用any:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.any(canBeInClub27))
true
複製代碼

注意,!all(不是全部)加上某個條件,能夠用any加上這個條件的取反來替換,反之亦然。爲了讓你的代碼更容易理解,應該選擇前面不須要否認符號的函數:

>>> val list = listOf(1, 2, 3)
>>> println(!list.all { it == 3 }) //!否認不明顯,這種狀況最好使用any
true
>>> println(list.any { it != 3 })  //lambda參數中的條件要取反
true
複製代碼

第一行檢查是保證不是全部元素都等於3.這和至少有一個元素不是3是一個意思,這正式你在第二行用any作的檢查。
若是你想知道有多少個元素知足了判斷式,使用count:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.count(canBeInClub27))
1
複製代碼

使用正確的函數完成工做:count VS size

count方法容易被遺忘,而後經過過濾集合以後再取大小來實現它:

>>> println(people.filter(canBeInClub27).size)
1
複製代碼

在這種狀況下,一箇中間集合會被建立並用來存儲全部知足判斷式的元素。而另外一方面,count方法只是跟蹤匹配元素的數量,不關心元素自己,因此更高效。

要找到一個知足判斷式的元素,使用find函數:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31))
>>> println(people.find(canBeInClub27))
Person(name=Hubert, age=26)
複製代碼

若是有多個匹配的元素就返回其中第一個元素;或者返回null,若是沒有一個元素能知足判斷式。find還有一個同義方法firstOrNull,可使用這個方法更清楚地表達你的意圖。

groupBy:把列表轉換成分組的map

假設你須要把全部元素按照不一樣的特徵劃分紅不一樣的分組。例如,你想把人按年齡分組,相同的年齡的人在一組。把這個特徵直接當作參數傳遞十分方便。groupBy函數能夠幫你作到這一點:

>>> val people = listOf(Person("Hubert", 26), Person("Bob", 31), Person("Carol", 31))
>>> println(people.groupBy { it.age })
複製代碼

此次操做的結果是一個map,是元素分組依據的鍵(這個例子中是age)和元素分組(persons)之間的映射:

{26=[Person(name=Hubert, age=26)], 31=[Person(name=Bob, age=31), Person(name=Carol, age=31)]}
複製代碼

每個分組都是存在一個列表中,結果的類型就是Map<Int, List<Person>> 。可使用像mapKeys和mapValues這樣的函數對這個map作進一步修改。
咱們再來看另一個例子,如何使用成員引用把字符串按照首字母分組:

>>> val list = listOf("a", "ab", "b")
>>> println(list.groupBy(String::first))
{a=[a, ab], b=[b]}
複製代碼

這裏的first並非String類的成員,而是一個擴展,也能夠把它當作成員引用訪問。

flatmap 和 flatten :處理嵌套集合中的元素

假設你有一堆書,使用Book類表示:

data class Book(val title: String, val authors: List<String>)
複製代碼

每本書均可能有一個或者多個做者,能夠統計出圖書館中的全部做者的set:

books,flatMap { it.authors }.toSet()
複製代碼

flatMap函數作了兩件事:首先根據做爲實參給定的函數對集合中每一個元素作變換(或者說映射),而後把多個列表合併(或者說平鋪)成一個列表。下面這個字符串的例子很好地闡明瞭這個概念:

>>> val strings = listOf("abc", "def")
>>> println(strings.flatMap { it.toList() })
[a, b, c, d, e, f]
複製代碼

字符串上的toList函數把它轉換成字符串列表。若是和toList一塊兒使用的是map函數,你會獲得一個字符列表的列表,就如同下圖的第二行。flapMap函數還會執行後面的步驟,並返回一個包含全部元素的列表。

應用flatMap函數以後的結果

讓咱們回到書籍做者的例子,每一本數均可能有多位做者,屬性book.authors存儲了每本書籍的做者集合,flatMap函數把全部書籍做者合併成了一個扁平的列表。toSet調用移除告終果集合中的全部重複元素。
當你卡殼在元素集合不得不合並一個的時候,你可能會想起flapMap來。若是你不須要作任何變換,只是須要平鋪一個集合,可使用flatten函數:listOfLists.flatten()

惰性集合操做:序列

在上一節,你看到了許多鏈式結合函數調用的例子,好比map和filter。這些函數會及早地建立中間集合,也就是說每一步的中間結果都被存儲在一個臨時列表。序列給了你執行這些操做的另外一種懸着,能夠避免建立這些臨時中間對象。
先來看個例子:

people.map(Person::name).filter { it.startsWith("A") }
複製代碼

Kotlin標準庫參考文檔有說明,filter和map都會返回一個列表。這意味着上面例子中的鏈式調用會建立兩個列表:一個保存filter函數的結果,另外一個保存map函數的結果。若是原列表只有兩個元素,這不是什麼問題,可是若是有一百萬個元素,鏈式調用就會變得十分低效。
爲了提升效率,能夠把操做變成使用序列,而不是直接使用集合:

people.asSequence()     //把初始集合轉換成序列
            .map(Person::name)
            .filter { it.startsWith("A") }
            .toList()   //把結果序列轉換回列表
複製代碼

應用此次操做後的結果和前面的例子如出一轍:一個以字母A開頭的人名列表。可是第二個例子沒有建立任何用於存儲元素的中間集合,因此元素數量巨大的狀況下性能將顯著提高。
Kotlin的惰性集合操做的入口就是Sequence接口。這個接口表示的就是一個能夠逐個列舉元素的元素序列。Sequence只提供了一個方法:iterator,用來從序列中獲取值。
Sequence接口的強大之處在於其操做的實現方式。序列中的元素求值是惰性的。所以,可使用序列更高效地對集合元素執行鏈式操做,而不須要穿件額外的集合來保存過程當中產生的中間結果。
能夠調用擴展函數asSequence把任意集合轉換成序列,調用toList來作反向的轉換。

執行序列操做:中間和末端操做

序列操做分爲兩類:中間的和末端的。一次中間操做返回的時另外一個序列,這個新序列知道如何變換原始序列中的元素。而一次末端操做返回的是一個結果,這個結果多是集合、元素、數字,或者其餘從初始集合的變換序列中獲取的任意對象。

//中間操做         //末端操做
people.asSequence().map{..}.filter {..}.toList() 
複製代碼

中間操做始終都是惰性的。先看看下面這個缺乏了末端操做的例子:

>>> listOf(1, 2, 3, 4).asSequence()
            .map { print("map($it)"); it *it }
            .filter { print("filter($it)");it % 2 == 0 }
複製代碼

執行這段代碼並不會再控制檯上輸出任何內容。這意味着map和filter變換被延期了,它們只有在獲取結果的時候纔會被應用(即末端操做調用的時候):

>>> listOf(1, 2, 3, 4).asSequence()
            .map { print("map($it)"); it *it }
            .filter { print("filter($it)");it % 2 == 0 }
            .toList()
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16)
複製代碼

末端操做觸發執行了全部的延期計算。
這個例子中另一件值得注意的事情是計算執行的順序。一個笨辦法是如今每一個元素上調用map函數,而後在結果序列的每一個元素上在調用filter函數。map和filter對集合就是這樣作的,而序列不同。對序列來講,全部操做是按順序應用在每個元素上的,處理完第一個元素(先映射在過濾),而後完成第二個元素的處理,以此類推。
這種方法意味着部分元素根本不會發生任何變換,若是在輪到它們以前就已經取得告終果。咱們來看一個map和find的例子。首先把一個數字映射成它的平方,而後找到第一個比數字3大的條目:

>>> println(listOf(1, 2, 3, 4)
            .map { print("map($it); "); it * it }
            .find { print("$it > 3 ?; "); it > 3 })
>>> println("----------------")
>>> println(listOf(1, 2, 3, 4).asSequence()
        .map { print("map($it); "); it * it }
        .find { print("$it > 3 ?; "); it > 3 })
        
map(1); map(2); map(3); map(4); 1 > 3 ?; 4 > 3 ?; 4
----------------
map(1); 1 > 3 ?; map(2); 4 > 3 ?; 4
複製代碼

第一種狀況,當你使用集合的時候,列表被變換成了另外一個lieb,因此map變換應用戴每個元素上,包括了數字3和4.而後第一個知足判斷式的元素被找到了:數字2的平方。
第二種狀況,find調用一開始就逐個地處理元素。從原始序列中取一個數字,用map變換它,而後在檢查它是知足傳給find的判斷式。當進行到數字2時,返現它的平方已經比3大,就把它做爲find操做結果返回了。再也不須要繼續檢查數字3和4,由於這以前你已經找到結果。
在集合上執行操做的順序也會影響性能。假設你有一個集合,想要打印集合中哪些長度小於某個限制的人名。你須要作兩件事:把每一個人映射成他們的名字,而後過濾掉其中哪些不夠短的名字。這種狀況能夠用任何順序應用map和filter操做。兩種順序獲得的結果同樣,但他們應該執行的變化總次數不同的:

>>> val people = listOf(Person("Hubert", 26), Person("Alice", 29),
            Person("Bob", 31), Person("Dan", 21))
>>> println(people.asSequence()
       .map { print("map(${it.name}); "); it.name }
        .filter { print("filter($it); ");it.length < 4 }
        .toList())
>>> println("----------------")
>>> println(people.asSequence()
        .filter { print("filter(${it.name}); ");it.name.length < 4 }
        .map { print("map($it); "); it.name }
        .toList())
        

map(Hubert); filter(Hubert); map(Alice); filter(Alice); map(Bob); filter(Bob); map(Dan); filter(Dan); [Bob, Dan]
----------------
filter(Hubert); filter(Alice); filter(Bob); map(Bob); filter(Dan); map(Dan); [Bob, Dan]
複製代碼

能夠看到,若是map在前,每一個元素都被變換。而若是filter在前,不合適的元素會被儘早地過濾掉且不會發生變換。

流 VS 序列 若是你很熟悉Java 8 中的流這個概念,你會發現序列就是它的翻版。Kotlin提供了這個概念本身的版本,緣由是Java 8的流並不支持哪些基於Java老版本的平臺,例如Android。若是你的目標版本是Java 8,流提供了一個Kotlin集合和序列目前尚未實現的重要特性:在多個CPU上並行執行流操做(好比map和filter)的能力。能夠根據Java的目標版本和你的特殊要求在流和序列之間作出選擇。

建立序列

前面的列表都是使用同一個方法建立序列:在集合上調用asSquence()。另外一個可能性是使用generateSequence函數。給定序列中的前一個元素,這個函數會計算出下一個元素。下面這個例子就是如何使用generateSequence計算100之內全部天然數之和。

>>> val naturalNumbers = generateSequence(0) { it + 1 }
>>> val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
>>> println(numbersTo100.sum())
5050
複製代碼

這個例子中的naturalNumbers和numbersTo100都是有延期操做的序列。這些序列中的實際數字直到你調用末端操做(這裏是sum)的時候纔會求值。
另外一種常見的用例是父序列。若是元素的父元素和它的類型相同(好比人類或者Java文件),你可能會對它全部祖先組成的序列的特質感興趣。下面這個例子能夠查詢文件是否放在隱藏目錄中,經過建立一個其父類目錄的序列並檢查每一個目錄的屬性來實現。

fun File.isInsideHiddenDirectory() = 
        generateSequence(this) { it.parentFile }.any{ it.isHidden}
>>> val file = File("/Users/svtk/.HiddenDir/a.txt")
true
複製代碼

你生成一個序列,經過提供第一個元素和獲取每一個後續元素的方式來實現。若是把any換成find,你還能夠獲得想要的那個目錄(對象)。注意,使用序列容許你找到須要的目錄以後當即中止遍歷目錄。

使用Java函數式接口

Kotlin的lambda也能夠無縫地和Java API互操做。在文章開頭,咱們就把lambda傳給Java方法的例子:

/* Kotlin */
button.setOnClickListener{ /* do someting */ }
複製代碼

Button類經過接收類型爲OnClickListner的實參的setOnClickListener方法給按鈕設置一個新的監聽器,在Java(8以前)中咱們不得不建立一個匿名類來做爲實參傳遞:

/* Java */
button.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //do something
        }
});
複製代碼

在Kotlin中,能夠傳遞一個lambda,代替這個例子:

/* Kotlin */
button.setOnClickListener{ view -> ... }
複製代碼

這個lambda用來實現OnClickListener,它有一個類型爲View的參數,和onclick方法同樣。
這種方法能夠工做的緣由是OnClickListener接口只有一個抽象方法。這種接口被稱爲函數式接口,或者SAM接口,SAM表明單抽象方法。Java API 中隨處可見像Runnable和Callable這樣的函數式接口,以及支持它們的方法。Kotlin容許你在調用接收函數式接口做爲參數的方法時使用lambda,來保證你的Kotlin代碼即整潔又符合習慣。

和Java不一樣,Kotlin擁有徹底的函數類型。正由於這樣,須要接收lambda做爲參數的Kotlin函數應該使用函數類型而不是函數式接口類型,做爲這些參數的類型。Kotlin不支持把lambda自動轉換成Kotlin接口對象。咱們會在以後的章節中討論聲明函數類型的用法。

是否是很是好奇把lambda傳給Java時到底發生了什麼,是如何銜接的?

把lambda當作參數傳遞給Java方法

能夠把lambda傳給任何指望函數式接口的方法。例以下面這個方法,它有一個Runnable類型的參數:

/* Java */
void postponeComputation(int delay, Runnable computation);
複製代碼

在Kotlin中,能夠調用它並把一個lambda做爲實參傳給它。編譯器會自動把它轉換成一個Runnable的實例。

postponeComputation(1000) { println(42) }
複製代碼

當咱們說一個Runnable的實例時,指的是一個實現了Runnable接口的匿名內部類的實例。編譯器會幫你建立它,並使用lambda做爲單抽象方法的方法體。
經過顯示的建立一個實現了Runnable的匿名對象也能達到一樣的效果:

postponeComputation(1000, object: Runnable {
    override fun run() {
        println(42)
    }
})
複製代碼

可是這裏有一點不同。當你顯式地聲明對象時,每次調用都會建立一個新的實例。使用lambda的狀況不一樣:若是lambda沒有訪問任何來自定義它的函數的變量,相應的匿名類實例能夠在屢次調用之間重用。
所以徹底等價的實現應該是下面這段代碼中顯示object聲明,它把Runnable實例存儲在一個變量中,而且每次調用的時候都使用這個變量:

val runnable = Runnable { println(42) }
fun handleComputation() {
    postponeComputation(1000, runnable)
}
複製代碼

若是lambda從包圍它的做用域中捕獲了變量,每次調用就再也不可能重用一同一個實例了。這種狀況下,每次調用時編譯器都要建立一個新對象,其中存儲着被捕獲的變量的值。

fun handleComputation(id: String) { //lambda會捕獲id這個變量
    postponeComputation(1000) { println(id) } //每次都建立一個Runnable新實例
}
複製代碼

Lambda的實現細節

自Kotlin 1.0起,每一個lambda表達式都會被編譯成一個匿名類,除非它是一個內聯lambda。後續版本計劃支持生成Java 8字節碼。一旦實現,編譯器就能夠避免爲每個lambda表達式都生成一個獨立的.class文件。若是lambda捕獲了變量,每一個被捕獲的變量會在匿名類中有對應的字段,並且每次調用都會建立一個這個匿名 類的新實例。不然,一個單例就會被建立。類的名稱由lambda聲明所在的函數名稱加上後綴衍生出來:上面一個例子就是HandleComputation$1。若是你反編譯以前lambda表達式的代碼,就會看到:

class HandleComputation$1(val id: String) : Runnable {
    override fun run() {
        println(42)
    }
}
fun handleComputation(id: String) {
    postponeComputation(100, HandleComputation$1(id))
}
複製代碼

如你所見,編譯器給每一個被捕捉的變量生成了一個字段和一個構造方法參數。

SAM構造方法:顯式地把lambda轉換成函數式接口

SAM構造方法是編譯器生成的函數,讓你執行從lambda到函數式接口實例的顯式轉換。能夠在編譯器不會自動應用轉換的上下文中使用它。例如,若是有一個方法返回的時一個函數式接口的實例,不能直接返回一個lambda,要用SAM構造方法把它包起來:

fun createAllDoneRunnable(): Runnable {
    return Runnable { println("All Done!") }
}
>>> createAllDoneRunnable().run()
All Done!
複製代碼

SAM構造方法的名稱和底層函數式接口的名稱同樣。SAM構造方法只接收一個參數——一個被用做函數式接口單抽象方法體的lambda,並返回實現了這個接口的類的一個實例。
除了返回值外,SAM構造方法還能夠用在須要把從lambda省城的函數式接口實例存儲在一個變量中的狀況。假設你要在多個按鈕上重用同一個監聽器,就像下面的代碼同樣:

val listener = OnClickListener { view ->
        val text = when(view.id) {
            R.id.button1 -> "First Button"
            R.id.button2 -> "Second Button"
            else -> "Unknown Button"
        }
        toast(text)
}
button1.setOnClickListener(listener)
button2.setOnClickListener(listener)
複製代碼

listener會檢查哪一個按鈕是點擊事件源並做出相應的行爲。可使用實現了OnClickListener的對象聲明來定義監聽器,可是SAM構造方法給你更簡潔的選擇。

Lambda和添加/移除監聽器

注意lambda內部沒有匿名對象那樣的this:沒有辦法引用到lambda轉換成的匿名類實例,從編譯器的角度來看,lambda是一個代碼塊,不是一個對象,並且也不能把它當成對象引用。Lambda中的this指向的是包圍它的類。
若是你的事件監聽器在處理事件時還須要取消它本身,不能使用lambda這樣作。這種狀況使用實現了接口的匿名對象,在匿名對象內,this關鍵字指向該對象實例,能夠把它傳給移除監聽器的API。

帶接收者的lambda:with 與 apply

with 函數

不少語法都有這樣的語句,能夠用它對同一個對象執行屢次操做,而不須要反覆把對象的名稱寫出來。Kotlin也不例外,但它提供的是一個叫with的庫函數,而不是某種特殊的語言結構。
要理解這種用法,咱們先看看下面這個例子,稍後你會用with來重構它:

fun alphabet(): String {
    val result = StringBuilder()
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\nNow I know the alphabet!")
    return result.toString()
}

>>> println(alphabet)
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Now I know the alphabet!
複製代碼

上面這個例子中,你調用了result實例上好幾個不一樣的方法,並且每次調用都要重複result這個名稱。這裏狀況還不算太糟,可是若是你用到的表達式更長或者重複得更多,該怎麼辦?
咱們來看看使用with函數重寫這段代碼:

fun alphabet(): String {
    val stringBuilder = StringBuilder()
    return with(stringBuilder) { //指定接收者的值,你會調用它的方法
        for (letter in 'A'..'Z') {
            this.append(letter)  //經過顯式地this來調用接收者值得方法
        } 
        append("\nNow I know the alphabet!") //this能夠省略
        this.toString() //從lambda返回
    }
}
複製代碼

with結構看起來像是一個特殊的語法結構,但它其實是一個接收兩個參數的函數:這個例子中兩個參數分別是stringBuilder和一個lambda。這裏利用了把lambda放在括號外的約定,這樣整個調用看起來就像是內建的語言功能。固然你也能夠選擇把它寫成with(stringBuilder, {...})
with函數把它的第一個參數轉換成第二個參數傳給他的lambda的接收者。能夠顯式地經過this引用來訪問這個接收者。或者能夠省略this引用,不用任何限定符直接訪問這個值得方法和屬性。這個例子中this指向了stringBuilder,這是傳給with的第一個參數。

帶接收者的lambda和擴展函數

你可能回憶起曾經見過類似的概念,this指向的時函數接收者。在擴展函數體內部,this指向了這個函數的那個類型的實例,並且也能夠被省略掉,讓你直接訪問接收者的成員。一個擴展函數某種意義上來講就是帶接收者的函數。

讓咱們進一步重構初始的alphabet函數,去掉額外的stringBuilder變量:

fun alphabet(): String = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
    toString()
}
複製代碼

如今這個函數只返回一個表達式,因此使用表達式函數體語法重寫了它。能夠建立一個新的StringBuilder實例直接當作實參傳給這個函數,而後在lambda中不須要顯示地this就能夠引用這個實例。

方法名衝突

若是你當作參數傳給with的對象已經有這樣的方法,該方法的名稱和你正在使用with的類中的方法同樣,怎麼辦?這種狀況下,能夠給this引用加上顯式地標籤來代表你要調用的時哪一個方法。假設函數的alphabet是類OuterClass的一個方法。若是你想引用的是定義在外部類的toString方法而不是StringBuilder,能夠用下面這種語句:

this@OuterClass.toString()
複製代碼

with返回的值是執行lambda代碼的結果,該結果就是lambda中的最後一個表達式(的值)。但有時候你想返回的是接收者對象,而不是執行lambda的結果。這時apply庫函數就派上用場了。

apply 函數

apply函數幾乎和with函數如出一轍,惟一的區別是apply始終會返回做爲實參傳遞給它的對象(接收者對象)。讓咱們再一次重構alphabet函數,這一次用的是apply:

fun alphabet(): String = StringBuilder().apply {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}.toString()
複製代碼

apply被聲明成一個擴展函數,他的接收者編程了做爲實參的lambda的接收者。執行apply的結果是StringBuilder,因此接下來你能夠調用toString把它轉換成String。
許多狀況下apply都頗有效,其中一種是在建立一個對象實例須要用正確的方式初始化它的一些屬性的時候。在Java中,這一般是經過另一個單獨的Builder對象來完成的,而在Kotlin中,能夠在任意對象上使用apply,徹底不須要任何任何來自定義該對象的庫的特別支持。
咱們來用apply演示一個Android中建立TextView實例的例子:

fun createViewWithCustomAttr(context: Context) = 
    TextView(context).apply {
        text = "Sample Text"
        textSize = 20.0f
        setPadding(10, 0, 0, 0)
    }
複製代碼

apply函數容許你使用緊湊的表達式函數體風格。新的TextView實例建立以後當即被傳給了apply。在傳給apply的lambda中,TextView實例變成了接收者,你就能夠調用它的方法並設置它的屬性。Lambda執行以後,apply返回已經初始化過的接收者實例,它變成了createViewWithCustomAttr函數的結果。
with函數和apply函數是最基本和最通用的使用帶接收者的lambda的例子。更多具體的函數函數也可使用這種模式。例如,你可使用標準庫函數buildString進一步簡化alphabet函數,它會負責建立StringBuilder並調用toString:

fun alphabet(): String = buildString {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
}
複製代碼
相關文章
相關標籤/搜索