90
技術社區[雲棲]
Kotlin的互操作——Kotlin與Java互相調用
互操作就是在Kotlin中可以調用其他編程語言的接口,隻要它們開放了接口,Kotlin就可以調用其成員屬性和成員方法,這是其他編程語言所無法比擬的。同時,在進行Java編程時也可以調用Kotlin中的API接口。
Kotlin與Java互操作
1 Kotlin調用Java
Kotlin在設計時就考慮了與Java的互操作性。可以從Kotlin中自然地調用現有的Java代碼,在Java代碼中也可以很順利地調用Kotlin代碼。
【例1】在Kotlin中調用Java的Util的list庫。
packagejqiang.Mutual.Kotlin
importjava.util.*
fundemo(source:List<Int>){
vallist=ArrayList<Int>()
for(iteminlist){
list.add(item)
}
for(iin0..source.size-1){
list[i]=source[i]
}
}
基本的互操作行為如下:
1.屬性讀寫
Kotlin可以自動識別Java中的getter/setter;在Java中可以過getter/setter操作Kotlin屬性。
【例2】自動識別Java中的getter/setter。
packagejqiang.Mutual.Kotlin
importjava.util.*
funmain(args:Array<String>){
valcalendar=Calendar.getInstance()
println(calendar.firstDayOfWeek)
if(calendar.firstDayOfWeek==1){// 調用getFirstDayOfWeek()方法
calendar.firstDayOfWeek=2// 調用setFirstDayOfWeek()方法
}
println(calendar.firstDayOfWeek)
}
遵循Java約定的getter和setter方法(名稱以get開頭的無參數方法和以set開頭的單參數方法)在Kotlin中表示為屬性。如果Java類隻有一個setter,那麼它在Kotlin中不會作為屬性可見,因為Kotlin目前不支持隻寫(set-only)屬性。
2.空安全類型
Kotlin的空安全類型的原理是,Kotlin在編譯過程中會增加一個函數調用,對參數類型或者返回類型進行控製,開發者可以在開發時通過注解@Nullable和@NotNull方式來彌補Java中空值異常。
Java中的任何引用都可能是null,這使得Kotlin對來自Java的對象進行嚴格的空安全檢查是不現實的。Java聲明的類型在Kotlin中稱為平台類型,並會被特別對待。對這種類型的空檢查要求會放寬,因此對它們的安全保證與在Java中相同。
【例3】空值實例。
vallist=ArrayList<String>()//非空(構造函數結果)
list.add("Item")
val size=list.size()//非空(原生Int)
Val item=list[0]//推斷為平台類型(普通Java對象)
當調用平台類型變量的方法時,Kotlin不會在編譯時報告可空性錯誤,但是在運行時調用可能會失敗,因為空指針異常。
item.substring(1)//允許,如果item==null可能會拋出異常
平台類型是不可標識的,這意味著不能在代碼中明確地寫下它們。當把一個平台值賦給一個Kotlin變量時,可以依賴類型推斷(該變量會具有所推斷出的平台類型,如上例中item所具有的類型),或者選擇我們所期望的類型(可空的或非空類型均可)。
val nullable:String?=item//允許,沒有問題
Val notNull:String=item//允許,運行時可能失敗
如果選擇非空類型,編譯器會在賦值時觸發一個斷言,這樣可以防止Kotlin的非空變量保存空值。當把平台值傳遞給期待非空值等的Kotlin函數時,也會觸發一個斷言。總的來說,編譯器盡力阻止空值通過程序向遠傳播(由於泛型的原因,有時這不可能完全消除)。
3.返回void的方法
如果在Java中返回void,那麼Kotlin返回的就是Unit。如果在調用時返回void,那麼Kotlin會事先識別該返回值為void。
4.注解的使用
@JvmField是Kotlin和Java互相操作屬性經常遇到的注解;@JvmStatic是將對象方法編譯成Java靜態方法;@JvmOverloads主要是Kotlin定義默認參數生成重載方法;@file:JvmName指定Kotlin文件編譯之後生成的類名。
5.NoArg和AllOpen
數據類本身屬性沒有默認的無參數的構造方法,因此Kotlin提供一個NoArg插件,支持JPA注解,如@Entity。AllOpen是為所標注的類去掉final,目的是為了使該類允許被繼承,且支持Spring注解,如@Componet;支持自定義注解類型,如@Poko。
6.泛型
Kotlin中的通配符“”代替Java中的“?”;協變和逆變由Java中的extends和super變成了out和in,如ArrayList;在Kotlin中沒有Raw類型,如Java中的List對應於Kotlin就是List<>。
與Java一樣,Kotlin在運行時不保留泛型,也就是對象不攜帶傳遞到它們的構造器中的類型參數的實際類型,即ArrayList()和ArrayList()是不能區分的。這使得執行is檢查不可能照顧到泛型,Kotlin隻允許is檢查星投影的泛型類型。
if(aisList<Int>)//錯誤:無法檢查它是否真的是一個Int列表
if(aisList<*>)//OK:不保證列表的內容
7.SAM轉換
就像Java 8一樣,Kotlin支持SAM轉換,這意味著Kotlin函數字麵值可以被自動轉換成隻有一個非默認方法的Java接口的實現,隻要這個方法的參數類型能夠與這個Kotlin函數的參數類型相匹配就行。
【例4】首先使用Java創建一個SAMInJava類,然後通過Kotlin調用Java中的接口。
Java代碼:
packagejqiang.Mutual.Java;
import java.util.ArrayList;
public class SAMInJava{
private ArrayList<Runnable>runnables=newArrayList<Runnable>();
public void addTask(Runnablerunnable){
runnables.add(runnable);
System.out.println("add:"+runnable+",size"+runnables.size());
}
Public void removeTask(Runnablerunnable){
runnables.remove(runnable);
System.out.println("remove:"+runnable+"size"+runnables.size());
}
}
Kotlin代碼:
packagejqiang.Mutual.Kotlin
importjqiang.Mutual.Java.SAMInJava
funmain(args:Array<String>){
varsamJava=SAMInJava()
vallamba={
print("hello")
}
samJava.addTask(lamba)
samJava.removeTask(lamba)
}
運行結果如下:
add:jqiang.Mutual.Kotlin.SamKt$sam$Runnable$bef91c64@63947c6bsize1
remove:jqiang.Mutual.Kotlin.SamKt$sam$Runnable$bef91c64@2b193f2dsize1
如果Java類有多個接受函數式接口的方法,那麼可以通過使用將Lambda表達式轉換為特定的SAM類型的適配器函數來選擇需要調用的方法。這些適配器函數也會按需由編譯器生成。
vallamba={
print("hello")
}
samJava.addTask(lamba)
SAM轉換隻適用於接口,而不適用於抽象類,即使這些抽象類隻有一個抽象方法。此功能隻適用於Java互操作;因為Kotlin具有合適的函數類型,所以不需要將函數自動轉換為Kotlin接口的實現,因此不受支持。
2 Java調用Kotlin
在Java中可以輕鬆地調用Kotlin代碼。
1.屬性
Kotlin屬性會被編譯成以下Java元素:
- getter方法,其名稱通過加前綴get得到;
- setter方法,其名稱通過加前綴set得到(隻適用於var屬性);
- 私有字段,與屬性名稱相同(僅適用於具有幕後字段的屬性)。
【例5】將Kotlin變量編譯成Java中的變量聲明。
Kotlin部分代碼:
varfirstName:String
Java部分代碼:
privateStringfirstName;
publicStringgetFirstName(){
returnfirstName;
}
publicvoidsetFirstName(StringfirstName){
this.firstName=firstName;
}
如果屬性名稱是以is開頭的,則使用不同的名稱映射規則:getter的名稱與屬性名稱相同,並且setter的名稱是通過將is替換成set獲得的。例如,對於屬性isOpen,其getter會稱作isOpen(),而其setter會稱作setOpen()。這一規則適用於任何類型的屬性,並不僅限於Boolean。
2.包級函數
在jqiang.Mutual.Kotlin包內的example.kt文件中聲明的所有函數和屬性,包括擴展函數,都被編譯成一個名為jqiang.Mutual.Kotlin.ExampleKt的Java類的靜態方法。
【例6】包級函數調用。
Kotlin部分代碼:
packagejqiang.Mutual.Kotlin
funbar(){
println("這隻是一個bar方法")
}
Java部分代碼:
packagejqiang.Mutual.Java;
publicclassexample{
publicstaticvoidmain(String[]args){
jqiang.Mutual.Kotlin.ExampleKt.bar();
}
}
可以使用@JvmName注解修改所生成的Java類的類名:
@file:JvmName("example")
packagejqiang.Mutual.Kotlin
那麼Java調用時就需要修改類名:
jqiang.Mutual.Kotlin.example.bar();
在多個文件中生成相同的Java類名(包名相同並且類名相同或者有相同的@JvmName注解)通常是錯誤的。然而,編譯器能夠生成一個單一的Java外觀類,它具有指定的名稱且包含來自於所有文件中具有該名稱的所有聲明。要生成這樣的外觀,請在所有的相關文件中使用@JvmMultifileClass注解。
@file:JvmName("example")
@file:JvmMultifileClass
packagejqiang.Mutual.Kotlin
3.實例字段
如果需要在Java中將Kotlin屬性作為字段暴露,那麼就需要使用@JvmField注解對其進行標注。該字段將具有與底層屬性相同的可見性。如果一個屬性有幕後字段(Backing Field)、非私有的、沒有open/override或者const修飾符,並且不是被委托的屬性,那麼可以使用@JvmField注解該屬性。
4.靜態方法
Kotlin將包級函數表示為靜態方法。如果對這些函數使用@JvmStatic進行標注,那麼Kotlin還可以為在命名對象或伴生對象中定義的函數生成靜態方法。如果使用該注解,那麼編譯器既會在相應對象的類中生成靜態方法,也會在對象自身中生成實例方法。例如:
classC{
companionobject{
@JvmStaticfunfoo(){}
funbar(){}
}
}
現在,foo()在Java中是靜態的,而bar()不是靜態的。
C.foo();//沒問題
C.bar();//錯誤:不是一個靜態方法
C.Companion.foo();//保留實例方法
C.Companion.bar();//唯一的工作方式
對於命名對象也同樣:
objectObj{
@JvmStaticfunfoo(){}
funbar(){}
}
在Java中:
Obj.foo();//沒問題
Obj.bar();//錯誤
Obj.INSTANCE.bar();//沒問題,通過單例實例調用
Obj.INSTANCE.foo();// 也沒問題
@JvmStatic注解也可以被應用於對象或伴生對象的屬性上,使其getter和setter方法在該對象或包含該伴生對象的類中是靜態成員。
5.可見性
Kotlin的可見性以下列方式映射到Java。
(1)private成員被編譯成private成員。
(2)private的頂層聲明被編譯成包級局部聲明。
(3)protected依然保持protected(注意,Java允許訪問同一個包中其他類的受保護成員,而Kotlin則不允許,所以Java類會訪問更廣泛的代碼)。
(4)internal聲明會成為Java中的public。internal類的成員會通過名字修飾,使其更難以在Java中被意外使用到,並且根據Kotlin規則使其允許重載相同簽名的成員而互不可見。
(5)public依然保持public。
6.空安全性
當從Java中調用Kotlin函數時,沒有任何方法可以阻止Kotlin中的空值傳入。Kotlin在JVM虛擬機中運行時會檢查所有的公共函數,可以檢查非空值,這時候就可以通過NullPointerException得到Java中的非空值代碼。
7.型變的泛型
當Kotlin使用了聲明處型變時,可以通過兩種方式從Java代碼中看到它們的用法。假設有以下類和兩個使用它的函數:
class Box<out T>(val value: T)
interface Base
class Derived : Base
fun boxDerived(value: Derived): Box<Derived> = Box(value)
fun unboxBase(box: Box<Base>): Base = box.value
將這兩個函數轉換成Java代碼:
Box<Derived> boxDerived(Derived value) { … }
Base unboxBase(Box<Base> box) { … }
在Kotlin中可以這樣寫:unboxBase(boxDerived("s")),但是在Java中是行不通的,因為在Java中Box類在其泛型參數T上是不型變的,於是Box並不是Box的子類。要使其在Java中工作,需要按以下方式定義unboxBase:
Base unboxBase(Box<? extends Base> box) { … }
這裏使用Java的通配符類型(? extends Base)通過使用處型變來模擬聲明處型變,因為在Java中隻能這樣。
當它作為參數出現時,為了讓Kotlin的API在Java中工作,對於協變定義的Box生成Box作為Box<?extendsSuper>(或者對於逆變定義的Foo生成Foo<?superBar>)。當它是一個返回值時,則不生成通配符;否則,Java客戶端必須處理它們(並且它違反了常用的Java編碼風格)。因此,將示例中的對應函數實際上翻譯如下:
// 作為返回類型——沒有通配符
Box<Derived> boxDerived(Derived value) { … }
// 作為參數——有通配符
Base unboxBase(Box<? extends Base> box) { … }
當參數類型是final時,生成通配符通常沒有意義,所以無論在什麼地方Box始終轉換為Box。
如果在默認不生成通配符的地方需要通配符,則可以使用@JvmWildcard注解。
fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value) // 將被轉換成
// Box<? extends Derived> boxDerived(Derived value) { … }
另外,如果根本不需要默認的通配符轉換,則可以使用@JvmSuppressWildcards注解。
fun unboxBase(box: Box<@JvmSuppressWildcards Base>):
Base = box.value
// 會翻譯成
// Base unboxBase(Box box) { … }
@JvmSuppressWildcards不僅可應用於單個類型參數,還可應用於整個聲明(如函數或類),從而抑製其中的所有通配符。
以上內容節選自《Kotlin開發快速入門與實戰》,點此鏈接可在博文視點官網查看此書。
想及時獲得更多精彩文章,可在微信中搜索“博文視點”或者掃描下方二維碼並關注。
最後更新:2017-11-03 10:34:13