1. 程式人生 > >Kotlin與Java互操作

Kotlin與Java互操作

互操作就是在Kotlin中可以呼叫其他程式語言的介面,只要它們開放了介面,Kotlin就可以呼叫其成員屬性和成員方法,這是其他程式語言所無法比擬的。同時,在進行Java程式設計時也可以呼叫Kotlin中的API介面。

Kotlin呼叫Java

Kotlin在設計時就考慮了與Java的互操作性。可以從Kotlin中自然地呼叫現有的Java程式碼,在Java程式碼中也可以很順利地呼叫Kotlin程式碼。例如,在Kotlin中呼叫Java的Util的list庫。

import java.util.*

fun demo(source: List<Int>) {
    val list = ArrayList<Int>()
    // “for”-迴圈用於 Java 集合:
for (item in source) { list.add(item) } // 操作符約定同樣有效: for (i in 0..source.size - 1) { list[i] = source[i] // 呼叫 get 和 set } }

基本的互操作行為如下:

屬性讀寫

Kotlin可以自動識別Java中的getter/setter函式,而在Java中可以過getter/setter操作Kotlin屬性。

import java.util.Calendar

fun calendarDemo() {
    val
calendar = Calendar.getInstance() if (calendar.firstDayOfWeek == Calendar.SUNDAY) { // 呼叫 getFirstDayOfWeek() calendar.firstDayOfWeek = Calendar.MONDAY // 呼叫ll setFirstDayOfWeek() } if (!calendar.isLenient) { // 呼叫 isLenient() calendar.isLenient = true
// 呼叫 setLenient() } }

循Java約定的getter和setter方法(名稱以get開頭的無引數方法和以set開頭的單引數方法)在Kotlin中表示為屬性。如果Java類只有一個setter,那麼它在Kotlin中不會作為屬性可見,因為Kotlin目前不支援只寫(set-only)屬性。

空安全型別

Kotlin的空安全型別的原理是,Kotlin在編譯過程中會增加一個函式呼叫,對引數型別或者返回型別進行控制,開發者可以在開發時通過註解@Nullable和@NotNull方式來限制Java中空值異常。
Java中的任何引用都可能是null,這使得Kotlin對來自Java的物件進行嚴格的空安全檢查是不現實的。Java宣告的型別在Kotlin中稱為平臺型別,並會被特別對待。對這種型別的空檢查要求會放寬,因此對它們的安全保證與在Java中相同。

val list = 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函式時,也會觸發一個斷言。總的來說,編譯器盡力阻止空值的傳播(由於泛型的原因,有時這不可能完全消除)。

平臺型別標識法

如上所述,平臺型別不能在程式中顯式表述,因此在語言中沒有相應語法。 然而,編譯器和 IDE 有時需要(在錯誤資訊中、引數資訊中等)顯示他們,Koltin提供助記符來表示他們:

  • T! 表示“T 或者 T?”;
  • (Mutable)Collection! 表示“可以可變或不可變、可空或不可空的 T 的 Java 集合”;
  • Array<(out) T>! 表示“可空或者不可空的 T(或 T 的子型別)的 Java 陣列”。

可空註解

由於泛型的原因,Kotlin在編譯時可能出現空異常,而使用空註解可以有效的解決這一情況。編譯器支援多種可空性註解:

  • JetBrains:org.jetbrains.annotations 包中的 @Nullable 和 @NotNull;
  • Android:com.android.annotations 和 android.support.annotations;
  • JSR-305:javax.annotation;
  • FindBugs:edu.umd.cs.findbugs.annotations;
  • Eclipse:org.eclipse.jdt.annotation;
  • Lombok:lombok.NonNull;

JSR-305 支援

在JSR-305中,定義的 @Nonnull 註解來表示 Java 型別的可空性。
如果 @Nonnull(when = …) 值為 When.ALWAYS,那麼該註解型別會被視為非空;When.MAYBE 與 When.NEVER 表示可空型別;而 When.UNKNOWN 強制型別為平臺型別。
可針對 JSR-305 註解編譯庫,但不需要為庫的消費者將註解構件(如 jsr305.jar)指定為編譯依賴。Kotlin 編譯器可以從庫中讀取 JSR-305 註解,並不需要該註解出現在類路徑中。

自 Kotlin 1.1.50 起, 也支援自定義可空限定符(KEEP-79)

型別限定符

如果一個註解型別同時標註有 @TypeQualifierNickname 與 JSR-305 @Nonnull(或者它的其他別稱,如 @CheckForNull),那麼該註解型別自身將用於 檢索精確的可空性,且具有與該可空性註解相同的含義。

@TypeQualifierNickname
@Nonnull(when = When.ALWAYS)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyNonnull {
}

@TypeQualifierNickname
@CheckForNull // 另一個型別限定符別稱的別稱
@Retention(RetentionPolicy.RUNTIME)
public @interface MyNullable {
}

interface A {
    @MyNullable String foo(@MyNonnull String x); 
    // 在 Kotlin(嚴格模式)中:`fun foo(x: String): String?`

    String bar(List<@MyNonnull String> x);       
    // 在 Kotlin(嚴格模式)中:`fun bar(x: List<String>!): String!`
}

型別限定符預設值

@TypeQualifierDefault 引入應用時在所標註元素的作用域內定義預設可空性的註解。這些註解型別應自身同時標註有 @Nonnull(或其別稱)與 @TypeQualifierDefault(…) 註解, 後者帶有一到多個 ElementType 值。

  • ElementType.METHOD 用於方法的返回值;
  • ElementType.PARAMETER 用於值引數;
  • ElementType.FIELD 用於欄位;
  • ElementType.TYPE_USE(自 1.1.60 起)適用於任何型別,包括型別引數、型別引數的上界與萬用字元型別。

當型別並未標註可空性註解時使用預設可空性,並且該預設值是由最內層標註有帶有與所用型別相匹配的 ElementType 的型別限定符預設註解的元素確定。

@Nonnull
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})
public @interface NonNullApi {
}

@Nonnull(when = When.MAYBE)
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE})
public @interface NullableApi {
}

@NullableApi
interface A {
    String foo(String x); // fun foo(x: String?): String?

    @NotNullApi // 覆蓋來自介面的預設值
    String bar(String x, @Nullable String y); // fun bar(x: String, y: String?): String 

    // 由於 `@NullableApi` 具有 `TYPE_USE` 元素型別,
    // 因此認為 List<String> 型別引數是可空的:
    String baz(List<String> x); // fun baz(List<String?>?): String?

    // “x”引數仍然是平臺型別,因為有顯式
    // UNKNOWN 標記的可空性註解:
    String qux(@Nonnull(when = When.UNKNOWN) String x); // fun baz(x: String!): String?
}

也支援包級的預設可空性:

@NonNullApi // 預設將“test”包中所有型別宣告為不可空
package test;

@UnderMigration 註解

庫的維護者可以使用 @UnderMigration 註解(在單獨的構件 kotlin-annotations-jvm 中提供)來定義可為空性型別限定符的遷移狀態。
@UnderMigration(status = …) 中的狀態值指定了編譯器如何處理 Kotlin 中註解型別的不當用法(例如,使用 @MyNullable 標註的型別值作為非空值):

  • MigrationStatus.STRICT 使註解像任何純可空性註解一樣工作,即對不當用法報錯並影響註解宣告內的型別在 Kotlin中的呈現;
  • 對於 MigrationStatus.WARN,不當用法報為警告而不是錯誤; 但註解宣告內的型別仍是平臺型別;
  • MigrationStatus.IGNORE 則使編譯器完全忽略可空性註解。

庫的維護者還可以將 @UnderMigration 狀態新增到型別限定符別稱與型別限定符預設值中。例如:

@Nonnull(when = When.ALWAYS)
@TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER})
@UnderMigration(status = MigrationStatus.WARN)
public @interface NonNullApi {
}

// 類中的型別是非空的,但是隻報警告
// 因為 `@NonNullApi` 標註了 `@UnderMigration(status = MigrationStatus.WARN)`
@NonNullApi 
public class Test {}

注意:可空性註解的遷移狀態並不會從其型別限定符別稱繼承,而是適用於預設型別限定符的用法。如果預設型別限定符使用型別限定符別稱,並且它們都標註有 @UnderMigration,那麼使用預設型別限定符的狀態。

返回void的方法

如果在Java中返回void,那麼Kotlin返回的就是Unit。如果在呼叫時返回void,那麼Kotlin會事先識別該返回值為void。

註解的使用

@JvmField是Kotlin和Java互相操作屬性經常遇到的註解;@JvmStatic是將物件方法編譯成Java靜態方法;@JvmOverloads主要是Kotlin定義預設引數生成過載方法;@file:JvmName指定Kotlin檔案編譯之後生成的類名。

NoArg和AllOpen

資料類本身屬性沒有預設的無引數的構造方法,因此Kotlin提供一個NoArg外掛,支援JPA註解,如@Entity。AllOpen是為所標註的類去掉final,目的是為了使該類允許被繼承,且支援Spring註解,如@Componet;支援自定義註解型別,如@Poko。

泛型

Kotlin 的泛型與 Java 有點不同,讀者可以具體參考泛型章節。Kotlin中的萬用字元“”代替Java中的“?”;協變和逆變由Java中的extends和super變成了out和in,如ArrayList;在Kotlin中沒有Raw型別,如Java中的List對應於Kotlin就是List<>。

與Java一樣,Kotlin在執行時不保留泛型,也就是物件不攜帶傳遞到它們的構造器中的型別引數的實際型別,即ArrayList()和ArrayList()是不能區分的。這使得執行is檢查不可能照顧到泛型,Kotlin只允許is檢查星投影的泛型型別。

if (a is List<Int>) // 錯誤:無法檢查它是否真的是一個 Int 列表
// but
if (a is List<*>) // OK:不保證列表的內容

Java陣列

與 Java 不同,Kotlin 中的陣列是不型變的。這意味著 Kotlin 不允許我們把一個 Array 賦值給一個 Array, 從而避免了可能的執行時故障。Kotlin 也禁止我們把一個子類的陣列當做超類的陣列傳遞給 Kotlin 的方法, 但是對於 Java 方法,這是允許的(通過 Array<(out) String>! 這種形式的平臺型別)。

Java 平臺上,陣列會使用原生資料型別以避免裝箱/拆箱操作的開銷。 由於 Kotlin 隱藏了這些實現細節,因此需要一個變通方法來與 Java 程式碼進行互動。 對於每種原生型別的陣列都有一個特化的類(IntArray、 DoubleArray、 CharArray 等等)來處理這種情況。 它們與 Array 類無關,並且會編譯成 Java 原生型別陣列以獲得最佳效能。

例如,假設有一個接受 int 陣列索引的 Java 方法。

public class JavaArrayExample {
    public void removeIndices(int[] indices) {
        // 在此編碼……
    }
}

在 Kotlin 中呼叫該方法時,你可以這樣傳遞一個原生型別的陣列。

val javaObj = JavaArrayExample()
val array = intArrayOf(0, 1, 2, 3)
javaObj.removeIndices(array)  // 將 int[] 傳給方法

當編譯為 JVM 位元組程式碼時,編譯器會優化對陣列的訪問,這樣就不會引入任何開銷。

val array = arrayOf(1, 2, 3, 4)
array[x] = array[x] * 2 // 不會實際生成對 get() 和 set() 的呼叫
for (x in array) { // 不會建立迭代器
    print(x)
}

即使當我們使用索引定位時,也不會引入任何開銷:

for (i in array.indices) {// 不會建立迭代器
    array[i] += 2
}

最後,in-檢測也沒有額外開銷:

if (i in array.indices) { // 同 (i >= 0 && i < array.size)
    print(array[i])
}

Java 可變引數

Java 類有時宣告一個具有可變數量引數(varargs)的方法來使用索引。

public class JavaArrayExample {
    public void removeIndicesVarArg(int... indices) {
        // 函式體……
    }
}

在這種情況下,你需要使用展開運算子 * 來傳遞 IntArray。

val javaObj = JavaArrayExample()
val array = intArrayOf(0, 1, 2, 3)
javaObj.removeIndicesVarArg(*array)

目前,無法傳遞 null 給一個宣告為可變引數的方法。

SAM轉換

就像Java 8一樣,Kotlin支援SAM轉換,這意味著Kotlin函式字面值可以被自動轉換成只有一個非預設方法的Java介面的實現,只要這個方法的引數型別能夠與這個Kotlin函式的引數型別相匹配就行。

首先使用Java建立一個SAMInJava類,然後通過Kotlin呼叫Java中的介面。

import java.util.ArrayList;
public class SAMInJava{
    private ArrayList<Runnable>runnables=new ArrayList<Runnable>();
    public void addTask(Runnable runnable){
        runnables.add(runnable);
System.out.println("add:"+runnable+",size"+runnables.size());
    }
    Public void removeTask(Runnable runnable){
        runnables.remove(runnable);
System.out.println("remove:"+runnable+"size"+runnables.size());
    }
}

然後在Kotlin中呼叫該Java介面。

fun main(args: Array<String>) {
    var samJava=SAMJava()
    val lamba={
        print("hello")
    }
    samJava.addTask(lamba)
    samJava.removeTask(lamba)
}

執行結果為:

add:SAMKotlinKt$sam$Runnable$8b8e16f1@4617c264,size1
remove:SAMKotlinKt$sam$Runnable$8b8e16f1@36baf30csize1

如果Java類有多個接受函式式介面的方法,那麼可以通過使用將Lambda表示式轉換為特定的SAM型別的介面卡函式來選擇需要呼叫的方法。

val lamba={
    print("hello")
}
samJava.addTask(lamba)

**注意:**SAM轉換隻適用於介面,而不適用於抽象類,即使這些抽象類只有一個抽象方法。此功能只適用於Java互操作;因為Kotlin具有合適的函式型別,所以不需要將函式自動轉換為Kotlin介面的實現,因此不受支援。

除此之外,Kotlin呼叫Java還有很多的內容,讀者可以通過下面的連結來了解:Kotlin呼叫Java

Java呼叫Kotlin

Java 可以輕鬆呼叫 Kotlin 程式碼。

屬性

Kotlin屬性會被編譯成以下Java元素:

  • getter方法,其名稱通過加字首get得到;
  • setter方法,其名稱通過加字首set得到(只適用於var屬性);
  • 私有欄位,與屬性名稱相同(僅適用於具有幕後欄位的屬性)。

例如,將Kotlin變數編譯成Java中的變數宣告。

private String firstName;

public String getFirstName() {
    return firstName;
}

public void setFirstName(String firstName) {
    this.firstName = firstName;
}

如果屬性名稱是以is開頭的,則使用不同的名稱對映規則:getter的名稱與屬性名稱相同,並且setter的名稱是通過將is替換成set獲得的。例如,對於屬性isOpen,其getter會稱作isOpen(),而其setter會稱作setOpen()。這一規則適用於任何型別的屬性,並不僅限於Boolean。

包級函式

例如,在org.foo.bar 包內的 example.kt 檔案中宣告的所有的函式和屬性,包括擴充套件函式, 該 類會編譯成一個名為 org.foo.bar.ExampleKt 的 Java 類的靜態方法。
首先,新建一個ExampleKt.kt的檔案,並新建一個bar函式:

package demo
class Foo
fun bar(){
    println("這只是一個bar方法")
}

然後,在Java中呼叫這個函式。

package demo;

public class Example {
    public static void main(String[]args){
        demo.ExampleKtKt.bar();
    }
}

當然,可以使用@JvmName註解修改所生成的Java類的類名。例如:

@file:JvmName("Demo")
package demo

那麼在Java呼叫時就需要修改類名。例如:

public class Example {
    public static void main(String[]args){
        demo.Demo.bar();
    }
}

在多個檔案中生成相同的Java類名(包名相同並且類名相同或者有相同的@JvmName註解)通常是錯誤的。然而,編譯器能夠生成一個單一的Java外觀類,它具有指定的名稱且包含來自於所有檔案中具有該名稱的所有宣告。要生成這樣的外觀,請在所有的相關檔案中使用@JvmMultifileClass註解。

@file:JvmName("example")
@file:JvmMultifileClass
package demo

例項欄位

如果需要在Java中將Kotlin屬性作為欄位暴露,那麼就需要使用@JvmField註解對其進行標註。使用@JvmField註解標註後,該欄位將具有與底層屬性相同的可見性。如果一個屬性有幕後欄位(Backing Field)、非私有的、沒有open/override或者const修飾符,並且不是被委託的屬性,那麼可以使用@JvmField註解該屬性。

首先,新建一個kt類,並新增如下程式碼。

class C(id: String) {
    @JvmField val ID = id
}

然後在Java中呼叫該程式碼,

class JavaClient {
    public String getID(C c) {
        return c.ID;
    }
}

延遲初始化的屬性(在Java中)也會暴露為欄位, 該欄位的可見性與 lateinit 屬性的 setter 相同。

靜態欄位

在命名物件或伴生物件時,宣告的 Kotlin 屬性會在該命名物件或包含伴生物件的類中包含靜態幕後欄位。通常這些欄位是私有的,但可以通過以下方式之一暴露出來。

  • @JvmField 註解;
  • lateinit 修飾符;
  • const 修飾符。

使用 @JvmField 標註的屬性,可以使其成為與屬性本身具有相同可見性的靜態欄位。例如:

class Key(val value: Int) {
    companion object {
        @JvmField
        val COMPARATOR: Comparator<Key> = compareBy<Key> { it.value }
    }
}

然後,在Java程式碼中呼叫屬性。

Key.COMPARATOR.compare(key1, key2);
// Key 類中的 public static final 欄位

在命名物件或者伴生物件中的一個延遲初始化的屬性具有與屬性 setter 相同可見性的靜態幕後欄位。

object Singleton {
    lateinit var provider: Provider
}

然後,在Java中使用該欄位的屬性。

// Java
Singleton.provider = new Provider();
// 在 Singleton 類中的 public static 非-final 欄位

用 const 標註的(在類中以及在頂層的)屬性在 Java 中會成為靜態欄位,首先新建一個kt檔案。

object Obj {
    const val CONST = 1
}
class C {
    companion object {
        const val VERSION = 9
    }
}
const val MAX = 239

然後,在Java中可以直接呼叫該屬性即可。

int c = Obj.CONST;
int d = ExampleKt.MAX;
int v = C.VERSION;

靜態方法

Kotlin將包級函式表示為靜態方法。如果對這些函式使用@JvmStatic進行標註,那麼Kotlin還可以為在命名物件或伴生物件中定義的函式生成靜態方法。如果使用該註解,那麼編譯器既會在相應物件的類中生成靜態方法,也會在物件自身中生成例項方法。例如:

class C {
    companion object {
        @JvmStatic fun foo() {}
        fun bar() {}
    }
}

現在,foo()在Java中是靜態的,而bar()不是靜態的。

C.foo(); // 正確
C.bar(); // 錯誤:不是一個靜態方法
C.Companion.foo(); // 保留例項方法
C.Companion.bar(); // 唯一的工作方式

對於命名物件,也存在同樣的規律。

object Obj {
    @JvmStatic fun foo() {}
    fun bar() {}
}

在 Java 中使用。

Obj.foo(); // 沒問題
Obj.bar(); // 錯誤
Obj.INSTANCE.bar(); // 沒問題,通過單例例項呼叫
Obj.INSTANCE.foo(); // 也沒問題

@JvmStatic 註解也可以應用於物件或伴生物件的屬性, 使其 getter 和 setter 方法在該物件或包含該伴生物件的類中是靜態成員。

可見性

Kotlin的可見性以下列方式對映到Java程式碼中。

  • private 成員編譯成 private 成員;
  • private 的頂層宣告編譯成包級區域性宣告;
  • protected 保持 protected(注意 Java 允許訪問同一個包中其他類的受保護成員, 而 Kotlin 不能,所以Java 類會訪問更廣泛的程式碼);
  • internal 宣告會成為 Java 中的 public。internal 類的成員會通過名字修飾,使其更難以在 Java 中意外使用到,並且根據 Kotlin 規則使其允許過載相同簽名的成員而互不可見;
  • public 保持 public。

KClass

有時你需要呼叫有 KClass 型別引數的 Kotlin 方法。 因為沒有從 Class 到 KClass 的自動轉換,所以你必須通過呼叫 Class.kotlin 擴充套件屬性的等價形式來手動進行轉換。例如:

kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class)

簽名衝突

有時我們想讓一個 Kotlin 中的命名函式在位元組碼中有另外一個 JVM 名稱,最突出的例子是由於型別擦除引發的。

fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>

這兩個函式不能同時定義在一個類中,因為它們的 JVM 簽名是一樣的。如果我們真的希望它們在 Kotlin 中使用相同的名稱,可以使用 @JvmName 去標註其中的一個(或兩個),並指定不同的名稱作為引數。例如:

fun List<String>.filterValid(): List<String>
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>

在 Kotlin 中它們可以用相同的名稱 filterValid 來訪問,而在 Java 中,它們分別是 filterValid 和 filterValidInt。同樣的技巧也適用於屬性中。例如:

val x: Int
    @JvmName("getX_prop")
    get() = 15

fun getX() = 10

生成過載

通常,如果你寫一個有預設引數值的 Kotlin 函式,在 Java 中只會有一個所有引數都存在的完整引數簽名的方法可見,如果希望向 Java 呼叫者暴露多個過載,可以使用 @JvmOverloads 註解。該註解可以用於建構函式、靜態方法中,但不能用於抽象方法和在介面中定義的方法。

class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) {
    @JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") {
        ……
    }
}

對於每一個有預設值的引數,都會生成一個額外的過載,這個過載會把這個引數和它右邊的所有引數都移除掉。在上例中,會生成以下程式碼 。

// 建構函式:
Foo(int x, double y)
Foo(int x)

// 方法
void f(String a, int b, String c) { }
void f(String a, int b) { }
void f(String a) { }

請注意,如次建構函式中所述,如果一個類的所有建構函式引數都有預設值,那麼會為其生成一個公有的無參建構函式,此時就算沒有 @JvmOverloads 註解也有效。

受檢異常

如上所述,Kotlin 沒有受檢異常。 所以,通常 Kotlin 函式的 Java 簽名不會宣告丟擲異常, 於是如果我們有一個這樣的 Kotlin 函式。首先,新建一個kt檔案。

//// example.kt
package demo
fun foo() {
    throw IOException()
}

然後,在 Java 中呼叫它的時候,需要使用try{}catch{}來捕捉這個異常。

// Java
try {
  demo.Example.foo();
}
catch (IOException e) { // 錯誤:foo() 未在 throws 列表中宣告 IOException
  // ……
}

因為 foo() 沒有宣告 IOException,我們從 Java 編譯器得到了一個報錯訊息。 為了解決這個問題,要在 Kotlin 中使用 @Throws 註解。

@Throws(IOException::class)
fun foo() {
    throw IOException()
}

空安全性

當從Java中呼叫Kotlin函式時,沒有任何方法可以阻止Kotlin中的空值傳入。Kotlin在JVM虛擬機器中執行時會檢查所有的公共函式,可以檢查非空值,這時候就可以通過NullPointerException得到Java中的非空值程式碼。

型變的泛型

當 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

// 作為返回型別——沒有萬用字元
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<Base> box) { …… }

注意:@JvmSuppressWildcards 不僅可用於單個型別引數,還可用於整個宣告(如函式或類),從而抑制其中的所有萬用字元。

Nothing 型別

型別 Nothing 是特殊的,因為它在 Java 中沒有自然的對應。確實,每個 Java 引用型別,包括 java.lang.Void 都可以接受 null 值,但是 Nothing 不行,以為這種型別不能在 Java 中被準確表示。這就是為什麼在使用 Nothing 引數的地方 Kotlin 生成一個原始型別:

fun emptyList(): List<Nothing> = listOf()
// 會翻譯成
// List emptyList() { …… }