在Kotlin中,函數做爲一等公民存在,函數能夠像值同樣被傳遞。lambda就是將一小段代碼封裝成匿名函數,以參數值的方式傳遞到函數中,供函數使用。java
在Java8以前,當外部須要設置一個類中某種事件的處理邏輯時,每每須要定義一個接口(類),並建立其匿名實例做爲參數,具體的處理邏輯存放到某個對應的方法中來實現:android
mName.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
複製代碼
mName.setOnClickListener {
}
複製代碼
上面的語法爲Kotlin的lambda表達式,都說lambda是匿名函數,匿名是知道了,但參數列表和返回類型呢?那若是這樣寫呢:性能優化
val sum = { x:Int, y:Int ->
x + y
}
複製代碼
lambda表達式始終用花括號包圍,並用 -> 將參數列表和函數主體分離。當lambda自行進行類型推導時,最後一行表達式返回值類型做爲lambda的返回值類型。如今一個函數必需的參數列表、函數體和返回類型都一一找出來了。bash
都說能夠將函數做爲變量值傳遞,那該變量的類型如何定義呢?函數變量的類型統稱函數類型,所謂函數類型就是聲明該函數的參數類型列表和函數返回值類型。閉包
先看個簡單的函數類型:app
() -> Unit
複製代碼
函數類型和lambda同樣,使用 -> 做分隔符,但函數類型是將參數類型列表和返回值類型分開,全部函數類型都有一個圓括號括起來的參數類型列表和返回值類型。ide
一些相對簡單的函數類型:函數
//無參、無返回值的函數類型(Unit 返回類型不可省略)
() -> Unit
//接收T類型參數、無返回值的函數類型
(T) -> Unit
//接收T類型和A類型參數、無返回值的函數類型(多個參數同理)
(T,A) -> Unit
//接收T類型參數,而且返回R類型值的函數類型
(T) -> R
//接收T類型和A類型參數、而且返回R類型值的函數類型(多個參數同理)
(T,A) -> R
複製代碼
較複雜的函數類型:佈局
(T,(A,B) -> C) -> R
複製代碼
一看有點複雜,先將(A,B) -> C抽出來,看成一個函數類型Y,Y = (A,B) -> C,整個函數類型就變成(T,Y) -> R。post
當顯示聲明lambda的函數類型時,能夠省去lambda參數列表中參數的類型,而且最後一行表達式的返回值類型必須與聲明的返回值類型一致:
val min:(Int,Int) -> Int = { x,y ->
//只能返回Int類型,最後一句表達式的返回值必須爲Int
//if表達式返回Int
if (x < y){
x
}else{
y
}
}
複製代碼
掛起函數屬於特殊的函數類型,掛起函數的函數類型中擁有 suspend 修飾符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C。(掛機函數屬於協程的知識,能夠暫且放過)
類型別名爲現有類型提供替代名稱。若是類型名稱太長,能夠另外引入較短的名稱,並使用新的名稱替代原類型名。類型別名不會引入新類型,它等效於相應的底層類型。使用類型別名爲函數類型起別稱:
typealias alias = (String,(Int,Int) -> String) -> String
typealias alias2 = () -> Unit
複製代碼
除了函數類型外,也能夠爲其餘類型起別名:
typealias FileTable<K> = MutableMap<K, MutableList<File>>
複製代碼
因爲Kotlin會根據上下文進行類型推導,咱們可使用更簡化的lambda,來實現更簡潔的語法。以maxBy函數爲例,該函數接受一個函數類型爲(T) -> R的參數:
data class Person(val age:Int,val name:String)
val persons = listOf(Person(17,"daqi"),Person(20,"Bob"))
//尋找年齡最大的Person對象
//花括號的代碼片斷表明lambda表達式,做爲參數傳遞到maxBy()方法中。
persons.maxBy( { person: Person -> person.age } )
複製代碼
persons.maxBy() { person: Person ->
person.age
}
複製代碼
persons.joinToString (" "){person ->
person.name
}
複製代碼
persons.maxBy{ person: Person ->
person.age
}
複製代碼
persons.maxBy{ person ->
person.age
}
複製代碼
由於maxBy()函數的聲明,參數類型始終與集合的元素類型相同,編譯器知道你對Person集合調用maxBy函數,因此能推導出lambda表達式的參數類型也是Person。
public inline fun <T, R : Comparable<R>> Iterable<T>.maxBy(selector: (T) -> R): T? {
}
複製代碼
但若是使用函數存儲lambda表達式,則沒法根據上下文推導出參數類型,這時必須顯式指定參數類型。
val getAge = { p:Person -> p.age }
//或顯式指定變量的函數類型
val getAge:(Person) -> Int = { p -> p.age }
複製代碼
persons.maxBy{
it.age
}
複製代碼
默認參數名稱it雖然簡潔,但不能濫用。當多個lambda嵌套的狀況下,最好顯式地聲明每一個lambda表達式的參數,不然很難搞清楚it引用的究竟是什麼值,嚴重影響代碼可讀性。
var persons:List<Person>? = null
//顯式指定參數變量名稱,不使用it
persons?.let { personList ->
personList.maxBy{ person ->
person.age
}
}
複製代碼
persons.joinToString (separator = " ",transform = {person ->
person.name
})
複製代碼
回看剛開始的setOnClickListener()方法,那接收的參數是一個接口實例,不是函數類型呀!怎麼就能夠傳lambda了呢?先了解一個概念:函數式接口:
函數式接口就是隻定義一個抽象方法的接口
SAM轉換就是將lambda顯示轉換爲函數式接口實例,但要求Kotlin的函數類型和該SAM(單一抽象方法)的函數類型一致。SAM轉換通常都是自動發生的。
SAM構造方法是編譯器爲了將lambda顯示轉換爲函數式接口實例而生成的函數。SAM構造函數只接收一個參數 —— 被用做函數式接口單抽象方法體的lambda,並返回該函數式接口的實例。
SAM構造方法的名稱和Java函數式接口的名稱同樣。
顯示調用SAM構造方法,模擬轉換:
#daqiInterface.java
//定義Java的函數式接口
public interface daqiInterface {
String absMethod();
}
#daqiJava.java
public class daqiJava {
public void setDaqiInterface(daqiInterface listener){
}
}
複製代碼
#daqiKotlin.kt
//調用SAM構造方法
val interfaceObject = daqiInterface {
//返回String類型值
"daqi"
}
//顯示傳遞給接收該函數式接口實例的函數
val daqiJava = daqiJava()
//此處不會報錯
daqiJava.setDaqiInterface(interfaceObject)
複製代碼
對interfaceObject進行類型判斷:
if (interfaceObject is daqiInterface){
println("該對象是daqiInterface實例")
}else{
println("該對象不是daqiInterface實例")
}
複製代碼
當單個方法接收多個函數式接口實例時,要麼所有顯式調用SAM構造方法,要麼所有交給編譯器自行轉換:
#daqiJava.java
public class daqiJava {
public void setDaqiInterface2(daqiInterface listener,Runnable runnable){
}
}
複製代碼
#daqiKotlin.kt
val daqiJava = daqiJava()
//所有交由編譯器自行轉換
daqiJava.setDaqiInterface2( {"daqi"} ){
}
//所有手動顯式SAM轉換
daqiJava.setDaqiInterface2(daqiInterface { "daqi" }, Runnable { })
複製代碼
注意:
目前講到的lambda都是普通lambda,lambda中還有一種類型:帶接收者的lambda。
帶接受者的lambda的類型定義:
A.() -> C
複製代碼
表示能夠在A類型的接收者對象上調用並返回一個C類型值的函數。
帶接收者的lambda好處是,在lambda函數體能夠無需任何額外的限定符的狀況下,直接使用接收者對象的成員(屬性或方法),亦可以使用this訪問接收者對象。
似曾相識的擴展函數中,this關鍵字也執行擴展類的實例對象,並且也能夠被省略掉。擴展函數某種意義上就是帶接收者的函數。
擴展函數和帶接收者的lambda極爲類似,雙方都須要一個接收者對象,雙方均可以直接調用該對象的成員。若是將普通lambda看成普通函數的匿名方式來看看待,那麼帶接收者類型的lambda能夠看成擴展函數的匿名方式來看待。
Kotlin的標準庫中就有提供帶接收者的lambda表達式:with和apply
val stringBuilder = StringBuilder()
val result = with(stringBuilder){
append("daqi在努力學習Android")
append("daqi在努力學習Kotlin")
//最後一個表達式做爲返回值返回
this.toString()
}
//打印結果即是上面添加的字符串
println(result)
複製代碼
with函數,顯式接收接收者,並將lambda最後一個表達式的返回值做爲with函數的返回值返回。
查看with函數的定義:
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
}
複製代碼
其lambda的函數類型表示,參數類型和返回值類型能夠爲不一樣值,也就是說能夠返回與接收者類型不一致的值。
apply函數幾乎和with函數如出一轍,惟一區別是apply始終返回接收者對象。對with的代碼進行重構:
val stringBuilder = StringBuilder().apply {
append("daqi在努力學習Android")
append("daqi在努力學習Kotlin")
}
println(stringBuilder.toString())
複製代碼
查看apply函數的定義:
public inline fun <T> T.apply(block: T.() -> Unit): T {
}
複製代碼
函數被聲明爲T類型的擴展函數,並返回T類型的對象。因爲其泛型的緣故,能夠在任何對象上使用apply。
apply函數在建立一個對象並須要對其進行初始化時很是有效。在Java中,通常藉助Builder對象。
val languages = listOf("Java","Kotlin","Python","JavaScript")
languages.filter {
it.contains("Java")
}.forEach{
println(it)
}
複製代碼
//替代View.OnClickListener接口
mName.setOnClickListener {
}
//替代Runnable接口
mHandler.post {
}
複製代碼
//定義函數
fun daqi(string:(Int) -> String){
}
//使用
daqi{
}
複製代碼
前面說lambda通常是將lambda中最後一個表達式的返回值做爲lambda的返回值,這種返回是隱式發生的,不須要額外的語法。但當多個lambda嵌套,須要返回外層lambda時,可使用有限返回。
有限返回就是帶標籤的return
複製代碼
標籤通常是接收lambda實參的函數名。當須要顯式返回lambda結果時,可使用有限返回的形式將結果返回。例子:
val array = listOf("Java","Kotlin")
val buffer = with(StringBuffer()) {
array.forEach { str ->
if (str.equals("Kotlin")){
//返回添加Kotlin字符串的StringBuffer
return@with this.append(str)
}
}
}
println(buffer.toString())
複製代碼
lambda表達式內部禁止使用裸return,由於一個不帶標籤的return語句老是在用fun關鍵字聲明的函數中返回。這意味着lambda表達式中的return將從包含它的函數返回。
fun main(args: Array<String>) {
StringBuffer().apply {
//打印第一個daqi
println("daqi")
return
}
//打印第二個daqi
println("daqi")
}
複製代碼
結果是:第一次打印完後,便退出了main函數。
lambda表達式語法缺乏指定函數的返回類型的能力,當須要顯式指定返回類型時,可使用匿名函數。匿名函數除了名稱省略,其餘和常規函數聲明一致。
fun(x: Int, y: Int): Int {
return x + y
}
複製代碼
與lambda不一樣,匿名函數中的return是從匿名函數中返回。
在Java中,當函數內聲明一個匿名內部類或者lambda時候,匿名內部類能引用這個函數的參數和局部變量,但這些參數和局部變量必須用final修飾。Kotlin的lambda同樣也能夠訪問函數參數和局部變量,而且不侷限於final變量,甚至能修改非final的局部變量!Kotlin的lambda表達式是真正意思上的閉包。
fun daqi(func:() -> Unit){
func()
}
fun sum(x:Int,y:Int){
var count = x + y
daqi{
count++
println("$x + $y +1 = $count")
}
}
複製代碼
正常狀況下,局部變量的生命週期都會被限制在聲明該變量的函數中,局部變量在函數被執行完後就會被銷燬。但局部變量或參數被lambda捕捉後,使用該變量的代碼塊能夠被存儲並延遲執行。這是爲何呢?
當捕捉final變量時,final變量會被拷貝下來與使用該final變量的lambda代碼一塊兒存儲。而對於非final變量會被封裝在一個final的Ref包裝類實例中,而後和final變量同樣,和使用該變量lambda一塊兒存儲。當須要修改這個非final引用時,經過獲取Ref包裝類實例,進而改變存儲在該包裝類中的佈局變量。因此說lambda仍是隻能捕捉final變量,只是Kotlin屏蔽了這一層包裝。
查看源碼:
public static final void sum(final int x, final int y) {
//建立一個IntRef包裝類對象,將變量count存儲進去
final IntRef count = new IntRef();
count.element = x + y;
daqi((Function0)(new Function0() {
public Object invoke() {
this.invoke();
return Unit.INSTANCE;
}
public final void invoke() {
//經過包裝類對象對內部的變量進行讀和修改
int var10001 = count.element++;
String var1 = x + " + " + y + " +1 = " + count.element;
System.out.println(var1);
}
}));
}
複製代碼
注意: 對於lambda修改局部變量,只有在該lambda表達式被執行的時候觸發。
lambda能夠將代碼塊做爲參數傳遞給函數,但當我須要傳遞的代碼已經被定義爲函數時,該怎麼辦?難不成我寫一個調用該函數的lambda?Kotlin和Java8容許你使用成員引用將函數轉換成一個值,而後傳遞它。
成員引用用來建立一個調用單個方法或者訪問單個屬性的函數值。
複製代碼
data class Person(val age:Int,val name:String)
fun daqi(){
val persons = listOf(Person(17,"daqi"),Person(20,"Bob"))
persons.maxBy({person -> person.age })
}
複製代碼
Kotlin中,當你聲明屬性的時候,也就聲明瞭對應的訪問器(即get和set)。此時Person類中已存在age屬性的訪問器方法,但咱們在調用訪問器時,還在外面嵌套了一層lambda。使用成員引用進行優化:
data class Person(val age:Int,val name:String)
fun daqi(){
val persons = listOf(Person(17,"daqi"),Person(20,"Bob"))
persons.maxBy(Person::age)
}
複製代碼
成員引用由類、雙冒號、成員三個部分組成:
頂層函數和擴展函數均可以使用成員引用來表示:
//頂層函數
fun daqi(){
}
//擴展函數
fun Person.getPersonAge(){
}
fun main(args: Array<String>) {
//頂層函數的成員引用(不附屬於任何一個類,類省略)
run(::daqi)
//擴展函數的成員引用
Person(17,"daqi").run(Person::getPersonAge)
}
複製代碼
還能夠對構造函數使用成員引用來表示:
val createPerson = ::Person
val person = createPerson(17,"daqi")
複製代碼
Kotlin1.1後,成員引用語法支持捕捉特定實例對象上的方法引用:
val personAge = Person(17,"name")::age
複製代碼
自Kotlin1.0起,每個lambda表達式都會被編譯成一個匿名類,帶來額外的開銷。可使用內聯函數來優化lambda帶來的額外消耗。
所謂的內聯函數,就是使用inline修飾的函數。在函數被使用的地方編譯器並不會生成函數調用的代碼,而是將函數實現的真實代碼替換每一次的函數調用。Kotlin中大多數的庫函數都標記成了inline。