Kotlin系列六:從集合談Kotlin中的Lambda編程

目錄

一 集合的函數式API

1.1.1 List

1.1.2 Set

1.1.3 Map

二 集合的函數式API

2.1 集合的函數式API的推導

2.1 集合常用函數式API

2.1.1 map函數

2.1.2 filter函數

2.1.3 any和all函數

三 Java函數式API的使用


一 集合的函數式API

集合的函數式API是學習Lambda編程的絕佳示例。

傳統意義上的集合主要就是List和Set,再廣泛一點的話,像Map這樣的鍵值對數據結構也可以包含進來。

List、Set和Map在Java中都是接口,List的主要實現類是ArrayList和LinkedList,Set的主要實現類是HashSet,Map的主要實現類是HashMap。

1.1 List

ArrayList的Java思維的Kotlin寫法:

val list = ArrayList<String>()
list.add("Apple")
list.add("Banana")
list.add("Orange")
list.add("Pear")
list.add("Grape")

這方式較煩瑣,Kotlin寫法:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")

for-in循環不僅可以用來遍歷區間,還可以用來遍歷集合:

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
    for (fruit in list) {
        println(fruit)
    }
}

注意:listOf()函數創建的是一個不可變的集合。

不可變的集合指的就是該集合只能用於讀取,我們無法對集合進行添加、修改或刪除操作。Kotlin在不可變性方面控制得極其嚴格。創建可變的集合用mutableListOf()函數

fun main() {
    val list = mutableListOf("Apple", "Banana", "Orange", "Pear", "Grape")
    list.add("Watermelon")
    for (fruit in list) {
        println(fruit)
    }
}

1.2 Set

Set集合的用法和List的類似,只是將創建集合的方式換成了setOf()和mutableSetOf()函數而已。

val set = setOf("Apple", "Banana", "Orange", "Pear", "Grape")
for (fruit in set) {
    println(fruit)
}

需要注意,Set集合底層是使用hash映射機制來存放數據的,因此集合中的元素無法保證有序,這是和List集合最大的不同之處。

1.3 Map

Map是一種鍵值對形式的數據結構,因此在用法上和List、Set集合有較大的不同。傳統的Map用法是先創建一個HashMap的實例,然後將一個個鍵值對數據添加到Map中。

HashMap的Java思維的Kotlin寫法:

val map = HashMap<String, Int>()
map.put("Apple", 1)
map.put("Banana", 2)
map.put("Orange", 3)
map.put("Pear", 4)
map.put("Grape", 5)

在Kotlin中並不建議使用put()和get()方法來對Map進行添加和讀取數據操作,而是更加推薦使用一種類似於數組下標的語法結構:

val map = HashMap<String, Int>()
map["Apple"] = 1
map["Banana"] = 2
map["Orange"] = 3
map["Pear"] = 4
map["Grape"] = 5

最方便的是,類似List和Set,Kotlin也提供了mapOf()和mutableMapOf()函數。

val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)

這裏的鍵值對組合看上去好像是使用to這個關鍵字來進行關聯的,但其實to並不是關鍵字,而是一個infix函數。

如何遍歷Map集合中的數據:

fun main() {
    val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
    for ((fruit, number) in map) {
        println("fruit is " + fruit + ", number is " + number)
    }
}

二 集合的函數式API

2.1 集合的函數式API的推導

集合的函數式API有很多個,我們重點學習函數式API的語法結構,也就是Lambda表達式的語法結構。

如何在一個水果集合裏面找到單詞最長的那個水果?

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
var maxLengthFruit = ""
for (fruit in list) {
    if (fruit.length > maxLengthFruit.length) {
        maxLengthFruit = fruit
    }
}
println("max length fruit is " + maxLengthFruit)

這段代碼很簡潔,但如果我們使用集合的函數式API,就可以讓這個功能變得更加容易:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val maxLengthFruit = list.maxBy { it.length }
println("max length fruit is " + maxLengthFruit)

Lambda的定義:Lambda就是一小段可以作爲參數傳遞的代碼

正常情況下,我們向某個函數傳參時只能傳入變量,而藉助Lambda卻允許傳入一小段代碼。到底多少代碼纔算一小段代碼呢?Kotlin對此並沒有進行限制,但是通常不建議在Lambda表達式中編寫太長的代碼,否則可能會影響代碼的可讀性。

接着我們來看一下Lambda表達式的語法結構:

{參數名1: 參數類型, 參數名2: 參數類型 -> 函數體}

這是Lambda表達式最完整的語法結構定義。首先最外層是一對大括號,如果有參數傳入到Lambda表達式中的話,我們還需要聲明參數列表,參數列表的結尾使用一個->符號,表示參數列表的結束以及函數體的開始,函數體中可以編寫任意行代碼(雖然不建議編寫太長的代碼),並且最後一行代碼會自動作爲Lambda表達式的返回值。

回到剛纔找出最長單詞水果的需求,前面使用的函數式API的語法結構看上去好像很特殊,但其實maxBy就是一個普通的函數而已,只不過它接收的是一個Lambda類型的參數,並且會在遍歷集合時將每次遍歷的值作爲參數傳遞給Lambda表達式。maxBy函數的工作原理是根據我們傳入的條件來遍歷集合,從而找到該條件下的最大值,比如說想要找到單詞最長的水果,那麼條件自然就應該是單詞的長度了。

理解了maxBy函數的工作原理之後,我們就可以開始套用剛纔學習的Lambda表達式的語法結構,並將它傳入到maxBy函數中了,如下所示:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val lambda = { fruit: String -> fruit.length }
val maxLengthFruit = list.maxBy(lambda)

可以看到,maxBy函數實質上就是接收了一個Lambda參數而已,並且這個Lambda參數是完全按照剛纔學習的表達式的語法結構來定義的,因此這段代碼應該算是比較好懂的。

這種寫法雖然可以正常工作,但是比較囉嗦,可簡化的點也非常多,下面我們就開始對這段代碼一步步進行簡化。

首先,我們不需要專門定義一個lambda變量,而是可以直接將lambda表達式傳入maxBy函數當中,因此第一步簡化如下所示:

val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length })

然後Kotlin規定,當Lambda參數是函數的最後一個參數時,可以將Lambda表達式移到函數括號的外面,如下所示:

val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length }

接下來,如果Lambda參數是函數的唯一一個參數的話,還可以將函數的括號省略:

val maxLengthFruit = list.maxBy { fruit: String -> fruit.length }

這樣代碼看起來就變得清爽多了吧?但是我們還可以繼續進行簡化。由於Kotlin擁有出色的類型推導機制,Lambda表達式中的參數列表其實在大多數情況下不必聲明參數類型,因此代碼可以進一步簡化成:

val maxLengthFruit = list.maxBy { fruit -> fruit.length }

最後,當Lambda表達式的參數列表中只有一個參數時,也不必聲明參數名,而是可以使用it關鍵字來代替,那麼代碼就變成了:

val maxLengthFruit = list.maxBy { it.length }

通過一步步推導的方式,我們就得到了和一開始那段函數式API一模一樣的寫法。

2.1 集合常用函數式API

2.1.1 map函數

map函數是最常用的一種函數式API,它用於將集合中的每個元素都映射成一個另外的值,映射的規則在Lambda表達式中指定,最終生成一個新的集合。比如,這裏我們希望讓所有的水果名都變成大寫模式,就可以這樣寫:

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
    val newList = list.map { it.toUpperCase() }
    for (fruit in newList) {
        println(fruit)
    }
}

map函數的功能非常強大,它可以按照我們的需求對集合中的元素進行任意的映射轉換,上面只是一個簡單的示例而已。除此之外,你還可以將水果名全部轉換成小寫,或者是隻取單詞的首字母,甚至是轉換成單詞長度這樣一個數字集合,只要在Lambda表示式中編寫你需要的邏輯即可。

2.1.2 filter函數

filter函數是用來過濾集合中的數據的,它可以單獨使用,也可以配合剛纔的map函數一起使用。

比如我們只想保留5個字母以內的水果,就可以藉助filter函數來實現,代碼如下所示:

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
    val newList = list.filter { it.length <= 5 }
                      .map { it.toUpperCase() }
    for (fruit in newList) {
        println(fruit)
    }
}

可以看到,這裏同時使用了filter和map函數,並通過Lambda表示式將水果單詞長度限制在5個字母以內。

另外值得一提的是,上述代碼中我們是先調用了filter函數再調用map函數。如果你改成先調用map函數再調用filter函數,也能實現同樣的效果,但是效率就會差很多,因爲這樣相當於要對集合中所有的元素都進行一次映射轉換後再進行過濾,這是完全不必要的。而先進行過濾操作,再對過濾後的元素進行映射轉換,就會明顯高效得多。

2.1.3 any和all函數

any函數用於判斷集合中是否至少存在一個元素滿足指定條件,all函數用於判斷集合中是否所有元素都滿足指定條件。

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
    val anyResult = list.any { it.length <= 5 }
    val allResult = list.all { it.length <= 5 }
    println("anyResult is " + anyResult + ", allResult is " + allResult)
}

這裏還是在Lambda表達式中將條件設置爲5個字母以內的單詞,那麼any函數就表示集合中是否存在5個字母以內的單詞,而all函數就表示集合中是否所有單詞都在5個字母以內。現在重新運行一下代碼:


三 Java函數式API的使用

Kotlin中調用Java方法時也可以使用函數式API,只不過這是有一定條件限制的。具體來講,如果我們在Kotlin代碼中調用了一個Java方法,並且該方法接收一個Java單抽象方法接口參數,就可以使用函數式API。Java單抽象方法接口指的是接口中只有一個待實現方法,如果接口中有多個待實現方法,則無法使用函數式API。

Java中子線程的例子:

public interface Runnable {
    void run();
}
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}).start();

Kotlin原始版本寫法:

Thread(object : Runnable {
    override fun run() {
        println("Thread is running")
    }
}).start()

目前Thread類的構造方法是符合Java函數式API的使用條件的,初步精簡:

Thread(Runnable {
    println("Thread is running") 
}).start()

如果一個Java方法的參數列表中不存在一個以上Java單抽象方法接口參數,我們還可以將接口名進行省略,再次精簡:

Thread({
    println("Thread is running") 
}).start()

Kotlin中當Lambda表達式是方法的最後一個參數時,可以將Lambda表達式移到方法括號的外面。同時,如果Lambda表達式還是方法的唯一一個參數,還可以將方法的括號省略,最終精簡結果:

Thread {
    println("Thread is running")
}.start()

或許你會覺得,既然本書中所有的代碼都是使用Kotlin編寫的,這種Java函數式API應該並不常用吧?其實並不是這樣的,因爲我們後面要經常打交道的Android SDK還是使用Java語言編寫的,當我們在Kotlin中調用這些SDK接口時,就很可能會用到這種Java函數式API的寫法。

舉個例子,Android中有一個極爲常用的點擊事件接口OnClickListener,其定義如下:

public interface OnClickListener {
    void onClick(View v);
}

可以看到,這又是一個單抽象方法接口。假設現在我們擁有一個按鈕button的實例,然後使用Java代碼去註冊這個按鈕的點擊事件,需要這麼寫:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});

而用Kotlin代碼實現同樣的功能,就可以使用函數式API的寫法來對代碼進行簡化,結果如下:

button.setOnClickListener {
}

可以看到,使用這種寫法,代碼明顯精簡了很多。這段給按鈕註冊點擊事件的代碼,我們在正式開始學習Android程序開發之後將會經常用到。

 

學習參考

1 官網文檔 https://www.kotlincn.net/docs/reference/basic-syntax.html

2 郭霖:《第一行代碼》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章