古詩詞大全網 - 個性簽名 - 為什麽 Kotlin 調用 java 時可以使用 Lambda

為什麽 Kotlin 調用 java 時可以使用 Lambda

1. Kotlin 中的 Lambda 表達式

如果妳已經開始使用 Koltin, 或者對它有過壹些了解的話,那麽壹定對這種寫法並不陌生了:

// 代碼壹:Kotlin 代碼view.setOnClickListener{

println("click")

}1234

它跟下面這段 Java 代碼是等價的:

// 代碼二:java 代碼view.setOnClickListener(new View.OnClickListener() { @Override

public void onClick(View v) {

System.out.println("click");

}

});1234567

和 Java8 壹樣,Kotlin 是支持 Lambda 表達式的,如代碼壹所示,就是 Lambda 的壹個具體應用。

可見,使用 lambda 減少了很多冗余,使代碼寫起來更簡潔優雅,讀起來也更順暢自然了。

但是,妳有沒有想過,為什麽 Kotlin 可以這樣寫,這裏為什麽可以使用 lambda ?

2. 為什麽可以這麽寫?

在 Kotlin 中,壹個 Lambda 就是壹個匿名函數。

代碼壹其實是對下面代碼三的簡寫:

// 代碼三:Kotlin 代碼view.setOnClickListener({

v -> println("click")

})1234

之所以簡寫成代碼壹的樣子,是基於這兩點特性:

如果 lambda 是壹個函數的唯壹參數,那麽調用這個函數時可以省略圓括號

如果 lambda 所表示的匿名函數只有壹個參數,那麽可以省略它的聲明以及->符號(默認會用it來給省略的參數名命名)

OK,從代碼三的結構中,能夠更清晰的看出,這裏的 view.setOnClickListener 函數是接收了壹個 lambda 作為參數。而在 Kotlin 中,什麽樣的函數才能把lambda(也即另壹個函數)作為參數呢?

—— 對,就是高階函數。

什麽是高階函數?

高階函數是將函數用作參數或返回值的函數。

這是 Kotlin 和 Java 的區別之壹,java 中並沒有高階函數的支持(java8是有高階函數的)。當我們在 java 中需要用到類似的概念時,通常的做法是傳遞壹個匿名類作為參數,然後實現其中的某些抽象方法 —— 就比如上面的代碼二。

事實上,如果在 Android Studio 中,從 Kotlin 的代碼查看 view.setOnClickListener 函數的定義,就會發現,看到的函數簽名就是壹個高階函數的定義:

函數簽名提示

如上圖,所看到函數簽名是:

public final fun setOnClickListener(l: ((v:View!)->Unit)!): Unit

當然,因為方法是在 Java 中定義的,所以它也列出了 Java 的聲明,是這樣:

public void setOnClickListener(OnClickListener l)

我們知道,Kotlin 跟 Java 的很多類型都有差異,所以它們在互相調用的時,會有壹個按照對應關系的轉換。

對於上面的對 setOnClickListener 方法的轉換,別的地方都好理解,比較難懂的是,為什麽會把參數從?OnClickListener?類型轉換成了?(View) -> Unit。

(View) -> Unit?是壹個函數類型,它表示這樣壹個函數:接收1個View類型的參數,返回Unit。

正是這個對參數類型的轉換,使得 setOnClickListener 方法在 Kotlin 中變成了壹個高階函數,這樣正是它之所以能夠使用 lambda 作為參數的原因。

而這種轉換,就是我們題目中所說到這篇文章的主角 ——?SAM 轉換?(Single Abstract Method Conversions)。

3. 什麽是 SAM 轉換?

好吧,說了這麽多,終於到正題了。

SAM 轉換,即 Single Abstract Method Conversions,就是對於只有單個非默認抽象方法接口的轉換 —— 對於符合這個條件的接口(稱之為?SAM Type?),在 Kotlin 中可以直接用 Lambda 來表示 —— 當然前提是 Lambda 的所表示函數類型能夠跟接口的中方法相匹配。

而?OnClickListener?在 java 中的定義是這樣的:

// 代碼四:OnClickListener 接口在 java 中的定義public interface OnClickListener { void onClick(View v);

}1234

—— 恰好它就是壹個符合條件的?SAM Type,onClick 函數的類型即是?(View) -> Unit。所以,在 Kotlin 中,能夠用 lambda 表達式?{ println("click")}?來代替?OnClickListener?作為 setOnClickListener 函數的參數。

4. SAM 轉換的歧義消除

SAM 轉換的存在,使得我們在 Kotlin 中調用 java 的時候能夠更得心應手了,它在大部分的時間都能工作的很好。

當然,也偶爾會有例外,比如,考慮下面的這段代碼:

// 代碼五public class TestSAM {

SamType1 sam1,;

SamType2 sam2,; public void setSam(SamType1 sam1) { this.sam1 = sam1;

} public void setSam(SamType2 sam2) { this.sam2 = sam2;

} public interface SamType1 { void doSomething(int value);

} public interface SamType2 { void doSomething2(int value);

}

}123456789101112131415161718

—— TestSAM 有兩個重載的 setSam 方法,

—— 並且它們的參數( SamType1、SamType2 )都是?SAM Type?的接口。

—— 並且 SamType1 跟 SamType2 的唯壹抽象方法的函數類型都是?(Int) -> Unit?。

o(╯□╰)o

這種情況比較吊軌,但是還有有可能會出現的。這時候,如果在 Kotlin 中直接使用代碼壹類似的方式,就會報錯了:

// 代碼六:kotlin中調用,這段代碼是編譯不過的TestSAM().setSam {

println("dodo") ?

}1234

會提示這裏歧義,編譯器不知道這個 Lambda 代表是 SamType1 跟 SamType2 中的哪壹個接口。

解決的辦法就是手動標明 Lambda 需要代替的接口類型,有兩種方式可以來標明:

// 代碼七: 歧義消除// 方式壹TestSAM().setSam (SamType1 { println("dodo") ?})

// 方式二TestSAM().setSam ({ println("dodo") } as SamType1)12345

當然,也還有壹種方法是不再使用 SAM 轉換的機制,而是直接使用壹個 SamType1 的實例作為參數:

// 代碼八: 使用壹個實現接口的匿名類作為參數TestSAM().setSam(object : TestSAM.SamType1 { override fun doSomething(value: Int) {

println("dodo")

}

})123456

這種方法當然也是可以的,只是跟 lambda 相比起來,就顯得不那麽優雅了(優雅很重要!!!)。

5. SAM 轉換的限制

SAM 轉換的限制主要有兩點 :

5.1 只支持 java

即只適用與 Kotlin 中對 java 的調用,而不支持對 Kotlin 的調用

官方的解釋是 Kotlin 本身已經有了函數類型和高階函數等支持,所以不需要了再去轉換了。

如果妳想使用類似的需要用 lambda 做參數的操作,應該自己去定義需要指定函數類型的高階函數。

5.2 只支持接口,不支持抽象類。

這個官方沒有多做解釋。

我想大概是為了避免混亂吧,畢竟如果支持抽象類的話,需要做強轉的地方就太多了。而且抽象類本身是允許有很多邏輯代碼在內部的,直接簡寫成壹個 Lambda 的話,如果出了問題去定位錯誤的難度也加大了很多。

6. 總結

OK,講完了。

總結起來就是?SAM 轉換就是 kotlin 在調用 java 代碼時能使用 Lambda 的原因。了解了其原理,能夠讓我們在寫代碼更自如,在偶爾出問題的時候也能更好更快地解決。