Kotlin的互操作——Kotlin與Java互相呼叫

博文視點發表於2017-11-03

互操作就是在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

相關文章