Kotlin調用Java代碼

Kotlin 在設計時就考慮了 Java 互操作性。可以從 Kotlin 中自然地調用現存的 Java 代碼,並且在 Java 代碼中也可以
很順利地調用 Kotlin 代碼。在本節中我們會介紹從 Kotlin 中調用 Java 代碼的一些細節。

幾乎所有 Java 代碼都可以使用而沒有任何問題

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
    }
}

Getter 和 Setter

遵循 Java 約定的 getter 和 setter 的方法(名稱以 get 開頭的無參數方法和
set 開頭的單參數方法)在 Kotlin 中表示爲屬性。 例如:

import java.util.Calendar

fun calendarDemo() {
    val calendar = Calendar.getInstance()
    if (calendar.firstDayOfWeek == Calendar.SUNDAY) {  // 調用 getFirstDayOfWeek()
        calendar.firstDayOfWeek = Calendar.MONDAY       // 調用 setFirstDayOfWeek()
    }
}

請注意,如果 Java 類只有一個 setter,它在 Kotlin 中不會作爲屬性可見,因爲 Kotlin 目前不支持只寫(set-only)屬性。

返回 void 的方法

如果一個 Java 方法返回 void,那麼從 Kotlin 調用時中返回 Unit
萬一有人使用其返回值,它將由 Kotlin 編譯器在調用處賦值,
因爲該值本身是預先知道的(是 Unit)。

將 Kotlin 中是關鍵字的 Java 標識符進行轉義

一些 Kotlin 關鍵字在 Java 中是有效標識符:in{: .keyword }、 object{: .keyword }、 is{: .keyword } 等等。
如果一個 Java 庫使用了 Kotlin 關鍵字作爲方法,你仍然可以通過反引號(`)字符轉義它
來調用該方法

foo.`is`(bar)

空安全和平臺類型

Java 中的任何引用都可能是 null{: .keyword },這使得 Kotlin 對來自 Java 的對象要求嚴格空安全是不現實的。
Java 聲明的類型在 Kotlin 中會被特別對待並稱爲平臺類型。對這種類型的空檢查會放寬,
因此它們的安全保證與在 Java 中相同(更多請參見下文)。

考慮以下示例:

val list = ArrayList<String>() // 非空(構造函數結果)
list.add("Item")
val size = list.size() // 非空(原生 int)
val item = list[0] // 推斷爲平臺類型(普通 Java 對象)

當我們調用平臺類型變量的方法時,Kotlin 不會在編譯時報告可空性錯誤,
但在運行時調用可能會失敗,因爲空指針異常或者 Kotlin 生成的阻止空值傳播的斷言:

item.substring(1) // 允許,如果 item == null 可能會拋出異常

平臺類型是不可標示的,意味着不能在語言中明確地寫下它們。
當把一個平臺值賦值給一個 Kotlin 變量時,可以依賴類型推斷(該變量會具有推斷出的的平臺類型,
如上例中 item 所具有的類型),或者我們可以選擇我們期望的類型(可空或非空類型均可):

val nullable: String? = item // 允許,沒有問題
val notNull: String = item // 允許,運行時可能失敗

如果我們選擇非空類型,編譯器會在賦值時觸發一個斷言。這防止 Kotlin 的非空變量保存
空值。當我們把平臺值傳遞給期待非空值等的 Kotlin 函數時,也會觸發斷言。
總的來說,編譯器盡力阻止空值通過程序向遠傳播(儘管鑑於泛型的原因,有時這
不可能完全消除)。

平臺類型表示法

如上所述,平臺類型不能在程序中顯式表述,因此在語言中沒有相應語法。
然而,編譯器和 IDE 有時需要(在錯誤信息中、參數信息中等)顯示他們,所以我們用
一個助記符來表示他們:

  • T! 表示「T 或者 T?」,
  • (Mutable)Collection<T>! 表示「可以可變或不可變、可空或不可空的 T 的 Java 集合」,
  • Array<(out) T>! 表示「可空或者不可空的 T(或 T 的子類型)的 Java 數組」

可空性註解

具有可空性註解的Java類型並不表示爲平臺類型,而是表示爲實際可空或非空的
Kotlin 類型。編譯器支持多種可空性註解,包括:

  • JetBrains
    (org.jetbrains.annotations 包中的 [@Nullable](https://github.com/Nullable "@Nullable")[@NotNull](https://github.com/NotNull "@NotNull"))
  • Android(com.android.annotationsandroid.support.annotations)
  • JSR-305(javax.annotation)
  • FindBugs(edu.umd.cs.findbugs.annotations)
  • Eclipse(org.eclipse.jdt.annotation)
  • Lombok(lombok.NonNull)。

你可以在 Kotlin 編譯器源代碼中找到完整的列表。

已映射類型

Kotlin 特殊處理一部分 Java 類型。這樣的類型不是「按原樣」從 Java 加載,而是 映射 到相應的 Kotlin 類型。
映射只發生在編譯期間,運行時表示保持不變。
Java 的原生類型映射到相應的 Kotlin 類型(請記住平臺類型):

Java 類型

Kotlin 類型

byte

kotlin.Byte

short

kotlin.Short

int

kotlin.Int

long

kotlin.Long

char

kotlin.Char

float

kotlin.Float

double

kotlin.Double

boolean

kotlin.Boolean

{:.zebra}

一些非原生的內置類型也會作映射:

Java 類型

Kotlin 類型

java.lang.Object

kotlin.Any!

java.lang.Cloneable

kotlin.Cloneable!

java.lang.Comparable

kotlin.Comparable!

java.lang.Enum

kotlin.Enum!

java.lang.Annotation

kotlin.Annotation!

java.lang.Deprecated

kotlin.Deprecated!

java.lang.CharSequence

kotlin.CharSequence!

java.lang.String

kotlin.String!

java.lang.Number

kotlin.Number!

java.lang.Throwable

kotlin.Throwable!

{:.zebra}

Java 的裝箱原始類型映射到可空的 Kotlin 類型:

Java type

Kotlin type

java.lang.Byte

kotlin.Byte?

java.lang.Short

kotlin.Short?

java.lang.Integer

kotlin.Int?

java.lang.Long

kotlin.Long?

java.lang.Char

kotlin.Char?

java.lang.Float

kotlin.Float?

java.lang.Double

kotlin.Double?

java.lang.Boolean

kotlin.Boolean?

{:.zebra}

請注意,用作類型參數的裝箱原始類型映射到平臺類型:
例如,List<java.lang.Integer> 在 Kotlin 中會成爲 List<Int!>

集合類型在 Kotlin 中可以是隻讀的或可變的,因此 Java 集合類型作如下映射:
(下表中的所有 Kotlin 類型都駐留在 kotlin.collections包中):

Java 類型

Kotlin 只讀類型

Kotlin 可變類型

加載的平臺類型

Iterator<T>

Iterator<T>

MutableIterator<T>

(Mutable)Iterator<T>!

Iterable<T>

Iterable<T>

MutableIterable<T>

(Mutable)Iterable<T>!

Collection<T>

Collection<T>

MutableCollection<T>

(Mutable)Collection<T>!

Set<T>

Set<T>

MutableSet<T>

(Mutable)Set<T>!

List<T>

List<T>

MutableList<T>

(Mutable)List<T>!

ListIterator<T>

ListIterator<T>

MutableListIterator<T>

(Mutable)ListIterator<T>!

Map<K, V>

Map<K, V>

MutableMap<K, V>

(Mutable)Map<K, V>!

Map.Entry<K, V>

Map.Entry<K, V>

MutableMap.MutableEntry<K,V>

(Mutable)Map.(Mutable)Entry<K, V>!

{:.zebra}

Java 的數組按下文所述映射:

Java 類型

Kotlin 類型

int[]

kotlin.IntArray!

String[]

kotlin.Array<(out) String>!

{:.zebra}

Kotlin 中的 Java 泛型

Kotlin 的泛型與 Java 有點不同(參見泛型)。當將 Java 類型導入 Kotlin 時,我們會執行一些轉換:

  • Java 的通配符轉換成類型投影

    • Foo<? extends Bar> 轉換成 Foo<out Bar!>!
    • Foo<? super Bar> 轉換成 Foo<in Bar!>!
  • Java的原始類型轉換成星投影

    • List 轉換成 List<*>!,即 List<out Any?>!

和 Java 一樣,Kotlin 在運行時不保留泛型,即對象不攜帶傳遞到他們構造器中的那些類型參數的實際類型。
ArrayList<Integer>()ArrayList<Character>() 是不能區分的。
這使得執行 is{: .keyword }-檢測不可能照顧到泛型。
Kotlin 只允許 is{: .keyword }-檢測星投影的泛型類型:

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

Java 數組

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

Java 平臺上,數組會使用原生數據類型以避免裝箱/拆箱操作的開銷。
由於 Kotlin 隱藏了這些實現細節,因此需要一個變通方法來與 Java 代碼進行交互。
對於每種原生類型的數組都有一個特化的類(IntArrayDoubleArrayCharArray 等等)來處理這種情況。
它們與 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{: .keyword }-檢測也沒有額外開銷

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

Java 可變參數

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

public class JavaArrayExample {

    public void removeIndices(int... indices) {
        // 在此編碼……
    }
}

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

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

目前無法傳遞 null{: .keyword } 給一個聲明爲可變參數的方法。

操作符

由於 Java 無法標記用於運算符語法的方法,Kotlin 允許
具有正確名稱和簽名的任何 Java 方法作爲運算符重載和其他約定(invoke() 等)使用。
不允許使用中綴調用語法調用 Java 方法。

受檢異常

在 Kotlin 中,所有異常都是非受檢的,這意味着編譯器不會強迫你捕獲其中的任何一個。
因此,當你調用一個聲明受檢異常的 Java 方法時,Kotlin 不會強迫你做任何事情:

fun render(list: List<*>, to: Appendable) {
    for (item in list) {
        to.append(item.toString()) // Java 會要求我們在這裏捕獲 IOException
    }
}

對象方法

當 Java 類型導入到 Kotlin 中時,類型 java.lang.Object 的所有引用都成了 Any
而因爲 Any 不是平臺指定的,它只聲明瞭 toString()hashCode()equals() 作爲其成員,
所以爲了能用到 java.lang.Object 的其他成員,Kotlin 要用到擴展函數。

wait()/notify()

Effective Java 第 69 條善意地建議優先使用併發工具(concurrency utilities)而不是 wait()notify()
因此,類型 Any 的引用不提供這兩個方法。
如果你真的需要調用它們的話,你可以將其轉換爲 java.lang.Object

(foo as java.lang.Object).wait()

getClass()

要取得對象的 Java 類,請在類引用上使用 java 擴展屬性。

val fooClass = foo::class.java

上面的代碼使用了自 Kotlin 1.1 起支持的綁定的類引用。你也可以使用 javaClass 擴展屬性。

val fooClass = foo.javaClass

clone()

要覆蓋 clone(),需要繼承 kotlin.Cloneable


class Example : Cloneable {
    override fun clone(): Any { …… }
}

不要忘記 Effective Java 的第 11 條: 謹慎地改寫clone

finalize()

要覆蓋 finalize(),所有你需要做的就是簡單地聲明它,而不需要 override{:.keyword} 關鍵字:

class C {
    protected fun finalize() {
        // 終止化邏輯
    }
}

根據 Java 的規則,finalize() 不能是 private{: .keyword } 的。

從 Java 類繼承

在 kotlin 中,類的超類中最多隻能有一個 Java 類(以及按你所需的多個 Java 接口)。

訪問靜態成員

Java 類的靜態成員會形成該類的「伴生對象」。我們無法將這樣的「伴生對象」作爲值來傳遞,
但可以顯式訪問其成員,例如:

if (Character.isLetter(a)) {
    // ……
}

Java 反射

Java 反射適用於 Kotlin 類,反之亦然。如上所述,你可以使用 instance::class.java,
ClassName::class.java 或者 instance.javaClass 通過 java.lang.Class 來進入 Java 反射。

其他支持的情況包括爲一個 Kotlin 屬性獲取一個 Java 的 getter/setter 方法或者幕後字段、爲一個 Java 字段獲取一個 KProperty、爲一個 KFunction 獲取一個 Java 方法或者構造函數,反之亦然。

SAM 轉換

就像 Java 8 一樣,Kotlin 支持 SAM 轉換。這意味着 Kotlin 函數字面值可以被自動的轉換成
只有一個非默認方法的 Java 接口的實現,只要這個方法的參數類型
能夠與這個 Kotlin 函數的參數類型相匹配。

你可以這樣創建 SAM 接口的實例:

val runnable = Runnable { println("This runs in a runnable") }

……以及在方法調用中:

val executor = ThreadPoolExecutor()
// Java 簽名:void execute(Runnable command)
executor.execute { println("This runs in a thread pool") }

如果 Java 類有多個接受函數式接口的方法,那麼可以通過使用
將 lambda 表達式轉換爲特定的 SAM 類型的適配器函數來選擇需要調用的方法。這些適配器函數也會按需
由編譯器生成。

executor.execute(Runnable { println("This runs in a thread pool") })

請注意,SAM 轉換隻適用於接口,而不適用於抽象類,即使這些抽象類也只有一個
抽象方法。

還要注意,此功能只適用於 Java 互操作;因爲 Kotlin 具有合適的函數類型,所以不需要將函數自動轉換
爲 Kotlin 接口的實現,因此不受支持。

在 Kotlin 中使用 JNI

要聲明一個在本地(C 或 C++)代碼中實現的函數,你需要使用 external 修飾符來標記它:

external fun foo(x: Int): Double

其餘的過程與 Java 中的工作方式完全相同。