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() { …… }