博客主頁java
高階函數就是以另外一個函數做爲參數或者返回值的函數。在kotlin中,函數能夠用lambda或函數引用來表示。
例如:標準庫中的filter函數將一個判斷式函數做爲參數,因此就是一個高階函數數據庫
list.filter { x > 0 }
爲了聲明一個以lambda做爲實參的函數,須要知道如何聲明對應的形參的類型。編程
先來看一個簡單的例子:把lambda表達式保存在局部變量中。segmentfault
// 有兩個Int類型參數和Int類型的返回值的函數 val sum = {x: Int, y: Int -> x + y} // 沒有參數和返回值的函數 val action = { println("32") }
編譯器會推導sum和action這兩個變量具備函數類型。這些變量的顯式類型聲明是什麼呢?安全
val sum: (Int, Int) -> Int = { x, y -> x + y } val action: () -> Unit = { println("32") }
聲明函數類型,須要將函數參數類型放在括號中,緊接着是一個箭頭和函數返回類型。一個函數類型聲明老是須要一個顯式的返回類型,在這裏Unit是不能省略的。app
在lambda表達式{ x, y -> x + y }中是如何省略參數x,y的類型的呢?由於它們的類型已經在函數類型的變量聲明中指定了,就不須要在lambda定義中重複聲明。ide
函數類型的返回值也能夠標記爲可空類型:函數
var canReturnNull: (Int, Int) -> Int? = { null }
也能夠定義一個函數類型的可空變量:post
var funOrNull: ((Int, Int) -> Int)? = null
知道了怎麼聲明高階函數,那如何去實現呢?性能
舉一個例子:定一個簡單的高階函數,實現兩個數字2和3的任意操做,而後打印結果
fun twoAndThree(operation: (Int, Int) -> Int) { // 調用函數類型的參數 val result = operation(2, 3) println("The result is $result") } twoAndThree { i, j -> i + j } // The result is 5 twoAndThree { i, j -> i * j } // The result is 6
調用做爲參數的函數和調用普通函數的語法是同樣的:把括號放在函數名後,並把參數放在括號中。
再舉一個例子:實現一個簡單版本的filter函數
其中filter函數是一個判斷式做爲參數。判斷式的類型是一個函數,以字符做爲參數並返回boolean類型的值。
fun String.filter(predicate: (Char) -> Boolean): String { val sb = StringBuilder() for (index in 0 until length) { val element = get(index) // 調用做爲參數傳遞給"predicate"函數 if (predicate(element)) sb.append(element) } return sb.toString() } // 傳遞一個lambda,做爲predicate參數 println("ab3d".filter { c -> c in 'a'..'z' }) // abd
背後的原理是:函數類型被聲明爲普通的接口,一個函數類型的變量是FunctionN接口的一個實現。
在kotlin標準庫定義了一系列的接口,這些接口對應於不一樣參數量的函數,Function0<R>(沒有參數的函數),Function1<P1, R>(一個參數的函數)等。每一個接口定義了一個invoke方法,調用這個方法就是執行函數。一個函數類型的變量就是實現了對應的FunctionN接口的實現類的實例,實現類的invoke方法包含了lambda函數體。
java8的lambda會被自動轉換爲函數類型的值
// kotlin 聲明 fun processTheAnswer(f: (Int) -> Int) { println(f(34)) } // java processTheAnswer.(number -> number + 1); // 35
在舊版的java中,能夠傳遞一個實現了接口函數中的invoke方法的匿名類的實例:
processTheAnswer(new Function1<Integer, Integer>() { @Override public Integer invoke(Integer integer) { // 在java代碼中使用函數類型(java8以前) return integer + 1; } });
在java中使用kotlin標準庫中以lambda做爲參數的函數,必須顯式的傳遞一個接受者對象做爲第一參數:
List<String> list = new ArrayList<>(); list.add("23"); // 能夠在java中使用kotlin標準庫中的函數 CollectionsKt.forEach(list, s -> { System.out.println(s); // 必須顯式的返回一個Unit類型的值 return Unit.INSTANCE; });
舉一個例子:使用了硬編碼toString轉換的joinToString函數
fun <T> Collection<T>.joinToString( separator: String = ", ", prefix: String = "", postfix: String = "" ): String { val result = StringBuilder(prefix) for ((index, element) in this.withIndex()) { if (index > 0) result.append(separator) // 使用默認的toString方法將對象轉換爲字符串 result.append(element) } result.append(postfix) return result.toString() }
將集合中的元素轉換爲字符串,老是使用toString方法。能夠定義一個函數類型的參數並用一個lambda做爲它的默認值
fun <T> Collection<T>.joinToString( separator: String = ", ", prefix: String = "", postfix: String = "", // 聲明一個以lambda爲默認值的函數類型的參數 transform: (T) -> String = { it.toString() } ): String { val result = StringBuilder(prefix) for ((index, element) in this.withIndex()) { if (index > 0) result.append(separator) // 調用做爲實參傳遞給 "transform" 形參的函數 result.append(transform(element)) } result.append(postfix) return result.toString() } val list = listOf("A", "B", "C") // 傳遞一個lambda做爲參數 println(list.joinToString { it.toLowerCase() }) // a, b, c
還能夠聲明一個參數爲可空的函數類型。可是不能直接調用做爲參數傳遞進來的函數,kotlin會由於檢測到潛在的空指針異常而致使編譯失敗。咱們能夠顯式地檢查null
fun foo(callback: (() -> Unit)?) { // ... if (callback != null) { callback() } }
能夠利用函數類型是一個包含invoke方法的接口的具體實現。
舉一個例子:使用函數類型的可空參數
fun <T> Collection<T>.joinToString( separator: String = ", ", prefix: String = "", postfix: String = "", // 聲明一個函數類型的可空參數 transform: ((T) -> String)? = null ): String { val result = StringBuilder(prefix) for ((index, element) in this.withIndex()) { if (index > 0) result.append(separator) // 使用安全調用語法調用函數 // 使用Elvis運算符處理回調沒有被指定的狀況 val str = transform?.invoke(element) ?: element.toString() result.append(str) } result.append(postfix) return result.toString() }
定義一個返回函數的函數:
enum class Delivery { STANDARD, EXPEDITED } class Order(val itemCount: Int) fun getShippingCostCalculator( delivery: Delivery ): (Order) -> Double { // 聲明一個返回函數的函數 if (delivery == Delivery.EXPEDITED) { // 返回lambda return { order -> 6 + 2.1 * order.itemCount } } return { order -> 1.2 * order.itemCount } } // 將返回的函數保存在變量中 val calculator = getShippingCostCalculator(Delivery.EXPEDITED) // 調用返回的函數 println("shipping costs ${calculator(Order(3))}") // shipping costs 12.3
data class SiteVisit( val path: String, val duration: Double, val os: OS ) enum class OS { IOS, ANDROID }
首先使用硬編碼的過濾器分析數據:
val list = listOf( SiteVisit("/", 22.0, OS.IOS), SiteVisit("/", 16.3, OS.ANDROID) ) val averageIOSDuration = list .filter { it.os == OS.IOS } .map(SiteVisit::duration) .average() println(averageIOSDuration) // 22.0
假設須要分析ANDROID數據,爲了不重複,能夠抽象一個參數。
用一個普通方法去除重複代碼:
// 將重複代碼抽取到函數中 fun List<SiteVisit>.averageDurationFor(os: OS) = filter { it.os == os }.map(SiteVisit::duration).average() println(list.averageDurationFor(OS.ANDROID)) // 16.3
用一個高階函數去除重複代碼:
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) = filter(predicate).map(SiteVisit::duration).average() println(list.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) }) // 19.15
lambda表達式會被正常地編譯成匿名類。這表示每調用一次lambda表達式,一個額外的類就會被建立,這會帶來運行時額外開銷。
有沒有可能讓編譯器生成跟java語句一樣高效的代碼,還能把重複的邏輯抽取到庫函數中呢?kotlin的編譯器可以作到。若是使用 inline 修飾符標記一個函數,在函數被使用的時候編譯器並不會生成函數調用的代碼,而是使用函數實現的餓真實代碼替換每一次的函數調用。
當一個函數被聲明爲inline時,它的函數體是內聯的,函數體會被直接替換到函數被調用的地方,而不是被正常調用。
// 定義一個內聯函數 inline fun <T> synchronized(lock: Lock, action: () -> T): T { lock.lock() try { return action() } finally { lock.unlock() } } val lock = ReentrantLock() synchronized(lock) { // ... }
由於已經將synchronized函數聲明爲inline,因此每次調用它所生成的代碼跟java的synchronized語句是同樣的。
fun foo(lock: Lock) { println("Before sync") synchronized(lock) { println("Action") } println("After sync") }
這段代碼編譯後的foo函數:
fun foo(lock: Lock) { println("Before sync") // 被內聯的synchronized函數代碼 lock.lock() try { println("Action") // 被內聯的lambda體代碼 } finally { lock.unlock() } println("After sync") }
由lambda生成的字節碼成爲了函數調用者定義的一部分,而不是被包含在一個實現了函數接口的匿名類中。
在調用內聯函數的時,也能夠傳遞函數類型的變量做爲參數:
class LockOwner( val lock: Lock ) { fun runUnderLock(body: () -> Unit) { // 傳遞一個函數類型的變量做爲參數,而不是一個lambda synchronized(lock, body) } }
在這種狀況下,lambda的代碼在內聯函數被調用點是不可用的,所以並不會被內聯。只有synchronized的函數體被內聯了,lambda纔會被正常調用。
runUnderLock函數會被編譯成相似於如下函數的字節碼:
class LockOwner( val lock: Lock ) { // 這個函數相似於真正的runUnderLock被編程成的字節碼 fun runUnderLock(body: () -> Unit) { lock.lock() try { // body沒有被內聯,由於在調用的地方尚未lambda body() } finally { lock.unlock() } } }
接下來看看kotlin標準庫操做集合的函數性能。
舉一個例子:使用lambda過濾一個集合
data class Person( val name: String, val age: Int ) val persons = listOf(Person("Alice", 23), Person("Bob", 43)) println(persons.filter { it.age < 30 }) // [Person(name=Alice, age=23)]
在kotlin中,filter函數被聲明爲內聯函數。意味者filter函數,以及傳遞給它的lambda的字節碼會被一塊兒內聯到filter被調用的地方。kotlin對內聯函數的支持,咱們能夠沒必要擔憂性能的問題。
使用inline關鍵字只能提升帶有lambda參數的函數的性能,其它的狀況須要額外的研究。
將帶有lambda參數的函數內聯,可以避免運行時開銷。其實不只節約了函數調用的開銷,並且節約了爲lambda建立匿名類,以及建立lambda實例對象的開銷。
在使用inline關鍵字的時,注意代碼的長度。若是內聯函數很大,將它的字節碼拷貝到每個調用點將會極大地增長字節碼的長度,應該將與lambda參數無關的代碼抽取到一個獨立的非內聯函數中。
lambda能夠去除重複代碼的一個常見模式是資源管理:先獲取一個資源,完成一個操做,而後釋放資源。資源能夠是一個文件,一個鎖,一個數據庫事務等。模式的標準作法是使用try/finally語句。
如前面實現的synchronized函數。但kotlin標準庫定義了另外一個叫做withLock函數,它是Lock接口的擴展函數。
val lock : Lock = ReentrantLock() // 在加鎖的狀況下執行指定的操做 lock.withLock { // ... } // 這是Kotlin庫中withLock函數的定義: // 須要加鎖的代碼抽取到一個獨立的方法中 public inline fun <T> Lock.withLock(action: () -> T): T { lock() try { return action() } finally { unlock() } }
在java中使用try-with-resource語句:
static String readFirstLineFromFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } }
在kotlin標準庫中可使用use函數:
fun readFirstLineFromFile(path: String): String { // 構建BufferedReader,調用use函數,傳遞一個lambda執行文件操做 BufferedReader(FileReader(path)).use { br -> return br.readLine() } }
use函數是一個擴展函數,被用來操做可關閉的資源,它接收一個lambda做爲參數。use函數也是內聯函數,不會引起任何性能開銷。
來比較兩種不一樣的遍歷集合的方法:
data class Person( val name: String, val age: Int ) val persons = listOf(Person("Alice", 23), Person("Bob", 43)) lookForAlice(persons) fun lookForAlice(persons: List<Person>) { for (person in persons) { if (person.name == "Alice") { println("Found!") return } } println("Alice is not Found!") } // Found!
若是使用forEach迭代重寫這段代碼安全嗎?使用forEach也是安全的。
fun lookForAlice(persons: List<Person>) { persons.forEach { if (it.name == "Alice") { println("Found!") return } } println("Alice is not Found!") } // Found!
若是在lambda中使用return關鍵字,它會從調用lambda的函數中返回,並不僅是從lambda中返回。這樣的return語句叫做非局部返回,由於它從一個比包含return的代碼塊更大的代碼塊中返回了。
只有在以lambda做爲參數的函數是內聯函數的時候才能從更外層的函數返回。forEach的函數體和lambda的函數體一塊兒被內聯了,因此在編譯的時候很容易作到從包含它的函數中返回。在一個非內聯函數的lambda中使用return表達式是不容許的。
也能夠在lambda表達式中使用局部返回。lambda中的局部返回跟for循環中的break表達式類似。它會終止lambda的執行,並接着從調用lambda的代碼處執行。
要區分局部返回仍是非局部返回,要用到標籤。若是想從一個lambda表達式處返回,能夠標記它,而後在return關鍵字後面引用這個標籤。
// 用一個標籤實現局部返回 fun lookForAlice(persons: List<Person>) { // 在lambda表達式上加標籤 persons.forEach label@{ if (it.name == "Alice") { // return@label引用了這個標籤 return@label } } println("Alice is not Found!") }
要標記一個lambda表達式,在lambda的花括號以前放一個標籤名(能夠是任何標識符),接着跟一個@符號。要從一個lambda返回,在return關鍵字後跟一個@符號,接着跟標籤名。
使用lambda做爲參數的函數的函數名能夠做爲標籤:
// 用函數名做爲return標籤 fun lookForAlice(persons: List<Person>) { persons.forEach { if (it.name == "Alice") { // return@forEach從lambda表達式返回 return@forEach } } println("Alice is not Found!") }
// 在匿名函數中使用return fun lookForAlice(persons: List<Person>) { // 使用匿名函數取代lambda表達式 persons.forEach(fun(person) { // return指向最近的函數:一個匿名函數 if (person.name == "Alice") return println("${person.name} is not Alice!") }) } // Bob is not Alice!
匿名函數省略了函數名和參數類型。
// 在filter中使用匿名函數 persons.filter(fun(person): Boolean { return person.age < 30 })
若是使用表達式函數體,就能夠省略返回值類型
// 使用表達式函數體匿名函數 persons.filter(fun(person) = person.age < 30)
在匿名函數中,不帶標籤的return表達式會從匿名函數返回,而不是從包含匿名函數的函數返回。return從最近的使用fun關鍵字聲明的函數返回。
lambda表達式沒有使用fun關鍵字,因此lambda中的return從最外層的函數返回。匿名函數使用了fun關鍵字,是從最近fun聲明的函數返回。
若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)