2021年5月13日
JDK版本 1.8、Idea版本 2021.1售滤、Kotlin版本 1.4.10、使用maven構建項目
實踐出真知
概念
kotlin接口中的方法支持默認值,與Java8中提供的default關鍵字作用一致
例如我們定義一個Human
接口:
interface Human {
fun name() = "Human"
}
其中name()
方法指定了默認值完箩,連default
都不用寫赐俗,就可以讓接口方法具有默認返回值。
起因
剛開始學kotlin的時候弊知,我粗略的看了下阻逮,就沒放在心上,直到實際上手秩彤,才發(fā)現(xiàn)用Java的class實現(xiàn)kotlin接口的時候叔扼,默認方法還是要顯式實現(xiàn)的!
寫個MaleHuman
實現(xiàn)該接口漫雷,發(fā)現(xiàn)IDEA報錯 ??
看來瓜富,name()
仍是一個抽象方法,用kotlin代碼實現(xiàn)的默認方法對Java編譯器“不可見”降盹,自然需要在Java代碼中實現(xiàn)這個抽象方法啦与柑!
趕忙用IDEA自動生成實現(xiàn)方法:
public class MaleHuman implements Human{
@NotNull
@Override
public String name() {
return Human.super.name();
}
}
OK,IDEA不報錯了澎现,跑個main方法先仅胞,然而。剑辫。干旧。編譯還是過不去 ??
探究
這時候想到,學習一門語言不僅要掌握語法結構妹蔽,還要了解代碼在經過編譯之后的執(zhí)行邏輯椎眯。
用jd-gui試著將字節(jié)碼反編譯成java代碼看下
很明顯胳岂,Kotlin編譯器首先會為接口類生成一個DefaultImpls
靜態(tài)內部類编整,這一點在編譯輸出目錄中也能看到
然后將我們代碼中寫的方法體作為這個內部類的同名靜態(tài)方法的方法體,不過還要傳一個實現(xiàn)類的對象進去乳丰。
看到這里掌测,再結合其他大佬的文章[1][2],可以得出反編譯工具給出的“偽Java代碼”少掉的部分产园,就是name()
方法被調用時汞斧,從抽象方法指向其內部類靜態(tài)方法的邏輯
進一步推測:從Java代碼中調用kotlin接口抽象方法時,編譯器(javac)“看到的”只是我們調用了一個abstract
方法什燕,這個方法并沒有被default
修飾奕筐,也沒有任何方法體鹦付,編譯器無法獲取更多的細節(jié)川蒙,干脆報錯著角,中斷編譯過程事富。
當然還是有很多似懂非懂的地方,比如為何在kotlin實現(xiàn)類里調用接口默認方法時可以通過編譯乘陪?JVM是如何處理kotlin接口的默認方法調用的统台?以及Kotlin為什么要采取這種方式生成字節(jié)碼?
簡單處理
這里就不再深挖了暂刘,既然知道kotlin編譯器會為接口的默認方法生成一個內部類饺谬,那我們拿來用就可以了
public class MaleHuman implements Human{
@NotNull
@Override
public String name() {
return Human.DefaultImpls.name(this);
}
}
直接調用內部類DefaultImpls
的name()
方法,傳入當前對象谣拣,然后編譯運行募寨,測試代碼如下:
public static void main(String[] args) {
MaleHuman male = new MaleHuman();
System.out.println(male.name());
}
成功輸出“Human”字符串。
通用方法
不過仔細想想森缠,每一個Java實現(xiàn)類都要寫這種模板代碼拔鹰,肯定不能滿足我們對于代碼整潔易維護的要求,還是查查有沒有更標準的方式吧
這時看到官方標準庫中提供了@JvmDefault
注解贵涵,正是我們需要的列肢,馬上加到name()
方法上試下,結果IDEA報錯
意思是這個注解必須在編譯時帶上-jvm-target 1.8這個參數(shù)宾茂,對應到maven工程就是需要在kotlin插件中添加jvmTarget參數(shù)
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
加上后再編譯瓷马,結果還是報錯。跨晴。
和剛才的一樣欧聘,都是少編譯選項,不同的是這次少了-Xjvm-default端盆,參考Stack Overflow上的答案[3]怀骤,還是在maven的kotlin插件中添加args參數(shù),附上完整版
<configuration>
<jvmTarget>1.8</jvmTarget>
<args>
<arg>-Xjvm-default=enable</arg>
</args>
</configuration>
(這里先設置成enable焕妙,下面再談原因)
加上后編譯不報錯了蒋伦,測試代碼也能夠正常輸出“Human”字符串
看下編譯后的代碼
靜態(tài)內部類不見了,抽象方法不見了焚鹊,留給我們的只是單單一個的name()
方法痕届,Java實現(xiàn)類也不需要顯式實現(xiàn)kotlin接口的默認方法了!并且IDEA自動生成的代碼也是支持的末患!
public class MaleHuman implements Human{
@NotNull
@Override
public String name() {
return Human.super.name();
}
}
后續(xù)
問題就此解決爷抓,kotlin接口中的默認方法終于也能像Java接口中的default
方法一樣被調用了,但是探尋的腳步還不能停阻塑,俗話說飲水思源,我們上kotlin官網看看@JvmDefault
這個注解的文檔[4]
此時發(fā)現(xiàn)官方文檔中給這一頁寫了一個大大的Deprecated,讓我們不要再用這個注解了,仔細閱讀后覺得Kotlin開發(fā)團隊這樣做是有原因的走搁,之前@JvmDefault
這個注解加上后独柑,根據(jù)傳遞給編譯器的參數(shù)不同,有兩種實現(xiàn)方式:
編譯器參數(shù) | 作用 |
---|---|
-Xjvm-default=enable | 注解方法會編譯成default 方法私植,內部類中的方法會被移除
|
-Xjvm-default=compatibility | 注解方法會編譯成default 方法忌栅,內部類中的方法仍會保留
|
以上是1.2.40版本時官方給出的解決方案,基于方法注解來處理曲稼。
那么問題來了索绪,如果一個接口中有多個默認方法呢?難道每個方法上都要加上這個注解才可以讓Java代碼正常調用嗎贫悄?
于是官方在1.4版本中調整了編譯器在處理接口默認方法時的作用范圍瑞驱,并給出了全新的編譯器參數(shù)
編譯器參數(shù) | 作用 |
---|---|
-Xjvm-default=all | 所有接口的默認方法都會生成default 方法且不會生成內部類 |
-Xjvm-default=all-compatibility | 所有接口的默認方法都會生成default 方法但仍會生成內部類 |
-Xjvm-default=compatibility | 與all-compatibility作用一致,主要是為了兼容1.4版本之前的參數(shù) |
還有一個新注解@JvmDefaultWithoutCompatibility
窄坦,是用在類上的唤反,用來告訴編譯器當前類或者接口中的默認方法無需生成DefaultImpls
內部類,應該是和上面的參數(shù)配合使用的鸭津。
新東西總要實踐一下彤侍,定義一個接口Something
,有三個默認方法逆趋,一個抽象方法
interface Something {
fun a() = "a"
fun b() = true
fun c() = 3
fun d()
}
經過測試盏阶,新方式不再需要修改代碼,直接在打包編譯時指定參數(shù)即可父泳!
- 當傳遞給編譯器-Xjvm-default=all參數(shù)時
三個方法都有了默認實現(xiàn)般哼,并且沒有生成內部類
- 當傳遞給編譯器-Xjvm-default=all-compatibility參數(shù)時
三個默認方法既出現(xiàn)在接口中,也出現(xiàn)在內部類里惠窄,而且內部類中的方法還加上了@Deprecated
注解
總結
- 避免在同一個工程中使用Java與Kotlin這兩種語言編寫代碼蒸眠。
盡管官方宣稱Kotlin提供了與Java的“一流”的互操作性,但是畢竟是兩種語言杆融,從文件楞卡、語法、結構到插件脾歇、編譯器蒋腮,甚至代碼運行時的行為都有差異,在開發(fā)過程中藕各,一定會不可避免地出現(xiàn)各種各樣的問題池摧,所以水平有限的話還是讓兩者(物理上)離得越遠越好;
- 如果實在避免不了激况,那就在模塊設計上避免作彤。
盡量只暴露方法或者API調用膘魄,而不是去繼承、實現(xiàn)對方的類或者接口竭讳,這樣可以將問題的發(fā)生場景最大限度地控制在可空性判斷上创葡。
參考文章
[1] 【Kotlin填坑-08】object 用法以及 Kotlin 的反編譯
[3] kotlin - @JvmDefault and how add compiler option - Stack Overflow