在 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 中表示爲屬性。
Boolean
訪問器方法(其中 getter 的名稱以 is
開頭而 setter 的名稱以 set
開頭)會表示爲與 getter 方法具有相同名稱的屬性。
例如:
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 類只有一個 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
和@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
)。
你可以在 Kotlin 編譯器源代碼中找到完整的列表。
註解類型參數
可以標註泛型類型的類型參數,以便同時爲其提供可空性信息。例如,考慮這些 Java 聲明的註解:
@NotNull
Set<@NotNull String> toSet(@NotNull Collection<@NotNull String> elements) { …… }
在 Kotlin 中可見的是以下簽名:
fun toSet(elements: (Mutable)Collection<String>) : (Mutable)Set<String> { …… }
請注意 String
類型參數上的 @NotNull
註解。如果沒有的話,類型參數會是平臺類型:
fun toSet(elements: (Mutable)Collection<String!>) : (Mutable)Set<String!> { …… }
標註類型參數適用於面向 Java 8 或更高版本環境,並且要求可空性註解支持 TYPE_USE
目標(org.jetbrains.annotations
15 或以上版本支持)。
注:由於當前的技術限制,IDE 無法正確識別用作依賴的已編譯 Java 庫中類型參數上的這些註解。
{:.note}
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)
(見下文)。
類型限定符別稱(自 1.1.50 起)
如果一個註解類型同時標註有
@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!`
}
類型限定符默認值(自 1.1.50 起)
@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?
}
注意:本例中的類型只在啓用了嚴格模式時出現,否則仍是平臺類型。參見
@UnderMigration
註解與編譯器配置兩節。
也支持包級的默認可空性:
// 文件:test/package-info.java
@NonNullApi // 默認將“test”包中所有類型聲明爲不可空
package test;
{:#undermigration-註解自-1160-起}
@UnderMigration
註解(自 1.1.60 起)
庫的維護者可以使用 @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
,那麼使用默認類型限定符的狀態。
編譯器配置
可以通過添加帶有以下選項的 -Xjsr305
編譯器標誌來配置 JSR-305 檢測:
-
-Xjsr305={strict|warn|ignore}
設置非@UnderMigration
註解的行爲。
自定義的可空性限定符,尤其是
@TypeQualifierDefault
已經在很多知名庫中流傳,而用戶更新到包含 JSR-305 支持的 Kotlin 版本時可能需要平滑遷移。自 Kotlin 1.1.60 起,這一標誌隻影響非@UnderMigration
註解。 -
-Xjsr305=under-migration:{strict|warn|ignore}
(自 1.1.60 起)覆蓋@UnderMigration
註解的行爲。
用戶可能對庫的遷移狀態有不同的看法:
他們可能希望在官方遷移狀態爲WARN
時報錯誤,反之亦然,他們可能希望推遲錯誤報告直到他們完成遷移。 -
-Xjsr305=@<fq.name>:{strict|warn|ignore}
(自 1.1.60 起)覆蓋單個註解的行爲,其中<fq.name>
是該註解的完整限定類名。對於不同的註解可以多次出現。這對於管理特定庫的遷移狀態非常有用。
其中 strict
、 warn
與 ignore
值的含義與 MigrationStatus
中的相同,並且只有 strict
模式會影響註解聲明中的類型在 Kotlin 中的呈現。
注意:內置的 JSR-305 註解
@Nonnull
、@Nullable
與@CheckForNull
總是啓用並影響所註解的聲明在 Kotlin 中呈現,無論如何配置編譯器的-Xjsr305
標誌。
例如,將 -Xjsr305=ignore -Xjsr305=under-migration:ignore [email protected]:warn
添加到編譯器參數中,會使編譯器對由
@org.library.MyNullable
標註的不當用法生成警告,而忽略所有其他 JSR-305 註解。
對於 kotlin 1.1.50+/1.2 版本,其默認行爲等同於 -Xjsr305=warn
。
strict
值應認爲是實驗性的(以後可能添加更多檢測)。
已映射類型
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.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.Character |
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}
注意:這些 Java 類型的靜態成員不能在相應 Kotlin 類型的伴生對象中直接訪問。要調用它們,請使用 Java 類型的完整限定名,例如 java.lang.Integer.toHexString(foo)
。
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 代碼進行交互。
對於每種原生類型的數組都有一個特化的類(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[1] = array[1] * 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 removeIndicesVarArg(int... indices) {
// 在此編碼……
}
}
在這種情況下,你需要使用展開運算符 *
來傳遞 IntArray
:
val javaObj = JavaArrayExample()
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()
類型 Any
的引用沒有提供 wait()
與 notify()
方法。通常不鼓勵使用它們,而建議使用 java.util.concurrent
。
如果確實需要調用這兩個方法的話,那麼可以將引用轉換爲 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》第三版 的第 13 條: 謹慎地改寫clone。
finalize()
要覆蓋 finalize()
,所有你需要做的就是簡單地聲明它,而不需要 override{:.keyword} 關鍵字:
class C {
protected fun finalize() {
// 終止化邏輯
}
}
根據 Java 的規則,finalize()
不能是 private{: .keyword } 的。
從 Java 類繼承
在 kotlin 中,類的超類中最多隻能有一個 Java 類(以及按你所需的多個 Java 接口)。
訪問靜態成員
Java 類的靜態成員會形成該類的“伴生對象”。我們無法將這樣的“伴生對象”作爲值來傳遞,
但可以顯式訪問其成員,例如:
if (Character.isLetter(a)) { …… }
要訪問已映射到 Kotlin 類型的 Java 類型的靜態成員,請使用 Java 類型的完整限定名:java.lang.Integer.bitCount(foo)
。
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 中的工作方式完全相同。
Java 中調用 Kotlin
Java 可以輕鬆調用 Kotlin 代碼。
例如,可以在 Java 方法中無縫創建與操作 Kotlin 類的實例。
然而,在將 Kotlin 代碼集成到 Java 中時,
需要注意 Java 與 Kotlin 之間的一些差異。
在本頁,我們會描述定製 Kotlin 代碼與其 Java 客戶端的互操作的方法。
屬性
Kotlin 屬性會編譯成以下 Java 元素:
- 一個 getter 方法,名稱通過加前綴
get
算出; - 一個 setter 方法,名稱通過加前綴
set
算出(只適用於var
屬性); - 一個私有字段,與屬性名稱相同(僅適用於具有幕後字段的屬性)。
例如,var firstName: String
編譯成以下 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.example
包內的 app.kt
文件中聲明的所有的函數和屬性,包括擴展函數,
都編譯成一個名爲 org.example.AppKt
的 Java 類的靜態方法。
// app.kt
package org.example
class Util
fun getTime() { /*……*/ }
// Java
new org.example.Util();
org.example.AppKt.getTime();
可以使用 @JvmName
註解修改生成的 Java 類的類名:
@file:JvmName("DemoUtils")
package org.example
class Util
fun getTime() { /*……*/ }
// Java
new org.example.Util();
org.example.DemoUtils.getTime();
如果多個文件中生成了相同的 Java 類名(包名相同並且類名相同或者有相同的
@JvmName
註解)通常是錯誤的。然而,編譯器能夠生成一個單一的 Java 外觀類,它具有指定的名稱且包含來自所有文件中具有該名稱的所有聲明。
要啓用生成這樣的外觀,請在所有相關文件中使用 @JvmMultifileClass
註解。
// oldutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package org.example
fun getTime() { /*……*/ }
// newutils.kt
@file:JvmName("Utils")
@file:JvmMultifileClass
package org.example
fun getDate() { /*……*/ }
// Java
org.example.Utils.getTime();
org.example.Utils.getDate();
實例字段
如果需要在 Java 中將 Kotlin 屬性作爲字段暴露,那就使用 @JvmField
註解對其標註。
該字段將具有與底層屬性相同的可見性。如果一個屬性有幕後字段(backing field)、非私有、沒有 open
/override
或者 const
修飾符並且不是被委託的屬性,那麼你可以用 @JvmField
註解該屬性。
class User(id: String) {
@JvmField val ID = id
}
// Java
class JavaClient {
public String getID(User user) {
return user.ID;
}
}
[延遲初始化的]https://www.kotlincn.net/docs/reference/(properties.html#延遲初始化屬性與變量)屬性(在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
Singleton.provider = new Provider();
// 在 Singleton 類中的 public static 非-final 字段
(在類中以及在頂層)以 const
聲明的屬性在 Java 中會成爲靜態字段:
// 文件 example.kt
object Obj {
const val CONST = 1
}
class C {
companion object {
const val VERSION = 9
}
}
const val MAX = 239
在 Java 中:
int const = Obj.CONST;
int max = ExampleKt.MAX;
int version = C.VERSION;
靜態方法
如上所述,Kotlin 將包級函數表示爲靜態方法。
Kotlin 還可以爲具名對象或伴生對象中定義的函數生成靜態方法,如果你將這些函數標註爲 @JvmStatic
的話。
如果你使用該註解,編譯器既會在相應對象的類中生成靜態方法,也會在對象自身中生成實例方法。
例如:
class C {
companion object {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
}
現在,callStatic()
在 Java 中是靜態的,而 callNonStatic()
不是:
C.callStatic(); // 沒問題
C.callNonStatic(); // 錯誤:不是一個靜態方法
C.Companion.callStatic(); // 保留實例方法
C.Companion.callNonStatic(); // 唯一的工作方式
對於具名對象也同樣:
object Obj {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
在 Java 中:
Obj.callStatic(); // 沒問題
Obj.callNonStatic(); // 錯誤
Obj.INSTANCE.callNonStatic(); // 沒問題,通過單例實例調用
Obj.INSTANCE.callStatic(); // 也沒問題
自 Kotlin 1.3 起,@JvmStatic
也適用於在接口的伴生對象中定義的函數。
這類函數會編譯爲接口中的靜態方法。請注意,接口中的靜態方法是 Java 1.8 中引入的,
因此請確保使用相應的編譯目標。
interface ChatBot {
companion object {
@JvmStatic fun greet(username: String) {
println("Hello, $username")
}
}
}
@JvmStatic
註解也可以應用於對象或伴生對象的屬性,
使其 getter 和 setter 方法在該對象或包含該伴生對象的類中是靜態成員。
接口中的默認方法
默認方法僅適用於面向 JVM 1.8 及更高版本。
{:.note}
@JvmDefault
註解在 Kotlin 1.3 中是實驗性的。其名稱與行爲都可能發生變化,導致將來不兼容。
{:.note}
自 JDK 1.8 起,Java 中的接口可以包含默認方法。
可以將 Kotlin 接口的非抽象成員爲實現它的 Java 類聲明爲默認。
如需將一個成員聲明爲默認,請使用 @JvmDefault
註解標記之。
這是一個帶有默認方法的 Kotlin 接口的一個示例:
interface Robot {
@JvmDefault fun move() { println("~walking~") }
fun speak(): Unit
}
默認實現對於實現該接口的 Java 類都可用。
//Java 實現
public class C3PO implements Robot {
// 來自 Robot 的 move() 實現隱式可用
@Override
public void speak() {
System.out.println("I beg your pardon, sir");
}
}
C3PO c3po = new C3PO();
c3po.move(); // 來自 Robot 接口的默認實現
c3po.speak();
接口的實現者可以覆蓋默認方法。
//Java
public class BB8 implements Robot {
//自己實現默認方法
@Override
public void move() {
System.out.println("~rolling~");
}
@Override
public void speak() {
System.out.println("Beep-beep");
}
}
爲了讓 @JvmDefault
生效,編譯該接口必須帶有 -Xjvm-default
參數。
根據添加註解的情況,指定下列值之一:
-Xjvm-default=enabled
只添加帶有@JvmDefault
註解的新方法時使用。
這包括爲 API 添加整個接口。-Xjvm-default=compatibility
將@JvmDefault
添加到以往 API 中就有的方法時使用。
這種模式有助於避免兼容性破壞:爲先前版本編寫的所有接口實現都會與新版本完全兼容。
然而,兼容模式可能會增大生成字節碼的規模並且影響性能。
關於兼容性的更多詳情請參見 @JvmDefault
參考頁。
在委託中使用
請注意,如果將帶有 @JvmDefault
的方法的接口用作委託,
那麼即是實際的委託類型提供了自己的實現,也會調用默認方法的實現。
interface Producer {
@JvmDefault fun produce() {
println("interface method")
}
}
class ProducerImpl: Producer {
override fun produce() {
println("class method")
}
}
class DelegatedProducer(val p: Producer): Producer by p {
}
fun main() {
val prod = ProducerImpl()
DelegatedProducer(prod).produce() // 輸出“interface method”
}
關於 Kotlin 中接口委託的更多詳情,請參見委託。
可見性
Kotlin 的可見性以下列方式映射到 Java:
private
成員編譯成private
成員;private
的頂層聲明編譯成包級局部聲明;protected
保持protected
(注意 Java 允許訪問同一個包中其他類的受保護成員,
而 Kotlin 不能,所以 Java 類會訪問更廣泛的代碼);internal
聲明會成爲 Java 中的public
。internal
類的成員會通過名字修飾,使其更難以在 Java 中意外使用到,並且根據 Kotlin 規則使其允許重載相同簽名的成員而互不可見;public
保持public
。
KClass
有時你需要調用有 KClass
類型參數的 Kotlin 方法。
因爲沒有從 Class
到 KClass
的自動轉換,所以你必須通過調用
Class<T>.kotlin
擴展屬性的等價形式來手動進行轉換:
kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class)
用 @JvmName
解決簽名衝突
有時我們想讓一個 Kotlin 中的具名函數在字節碼中有另外一個 JVM 名稱。
最突出的例子是由於類型擦除引發的:
fun List<String>.filterValid(): List<String>
fun List<Int>.filterValid(): List<Int>
這兩個函數不能同時定義,因爲它們的 JVM 簽名是一樣的:filterValid(Ljava/util/List;)Ljava/util/List;
。
如果我們真的希望它們在 Kotlin 中用相同名稱,我們需要用 @JvmName
去標註其中的一個(或兩個),並指定不同的名稱作爲參數:
fun List<String>.filterValid(): List<String>
@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int>
在 Kotlin 中它們可以用相同的名稱 filterValid
來訪問,而在 Java 中,它們分別是 filterValid
和 filterValidInt
。
同樣的技巧也適用於屬性 x
和函數 getX()
共存:
val x: Int
@JvmName("getX_prop")
get() = 15
fun getX() = 10
如需在沒有顯式實現 getter 與 setter 的情況下更改屬性生成的訪問器方法的名稱,可以使用 @get:JvmName
與 @set:JvmName
:
@get:JvmName("x")
@set:JvmName("changeX")
var x: Int = 23
生成重載
通常,如果你寫一個有默認參數值的 Kotlin 函數,在 Java 中只會有一個所有參數都存在的完整參數簽名的方法可見,如果希望向 Java 調用者暴露多個重載,可以使用
@JvmOverloads
註解。
該註解也適用於構造函數、靜態方法等。它不能用於抽象方法,包括在接口中定義的方法。
class Circle @JvmOverloads constructor(centerX: Int, centerY: Int, radius: Double = 1.0) {
@JvmOverloads fun draw(label: String, lineWidth: Int = 1, color: String = "red") { /*……*/ }
}
對於每一個有默認值的參數,都會生成一個額外的重載,這個重載會把這個參數和它右邊的所有參數都移除掉。在上例中,會生成以下代碼
:
// 構造函數:
Circle(int centerX, int centerY, double radius)
Circle(int centerX, int centerY)
// 方法
void draw(String label, int lineWidth, String color) { }
void draw(String label, int lineWidth) { }
void draw(String label) { }
請注意,如次構造函數中所述,如果一個類的所有構造函數參數都有默認值,那麼會爲其生成一個公有的無參構造函數。這就算沒有 @JvmOverloads
註解也有效。
受檢異常
如上所述,Kotlin 沒有受檢異常。
所以,通常 Kotlin 函數的 Java 簽名不會聲明拋出異常。
於是如果我們有一個這樣的 Kotlin 函數:
// example.kt
package demo
fun writeToFile() {
/*……*/
throw IOException()
}
然後我們想要在 Java 中調用它並捕捉這個異常:
// Java
try {
demo.Example.writeToFile();
}
catch (IOException e) { // 錯誤:writeToFile() 未在 throws 列表中聲明 IOException
// ……
}
因爲 writeToFile()
沒有聲明 IOException
,我們從 Java 編譯器得到了一個報錯消息。
爲了解決這個問題,要在 Kotlin 中使用 @Throws
註解。
@Throws(IOException::class)
fun writeToFile() {
/*……*/
throw IOException()
}
空安全性
當從 Java 中調用 Kotlin 函數時,沒人阻止我們將 null{: .keyword } 作爲非空參數傳遞。
這就是爲什麼 Kotlin 給所有期望非空參數的公有函數生成運行時檢測。
這樣我們就能在 Java 代碼裏立即得到 NullPointerException
。
型變的泛型
當 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<Derived>
並不是 Box<Base>
的子類。
要使其在 Java 中工作,我們按以下這樣定義 unboxBase
:
Base unboxBase(Box<? extends Base> box) { …… }
這裏我們使用 Java 的通配符類型(? extends Base
)來通過使用處型變來模擬聲明處型變,因爲在 Java 中只能這樣。
當它作爲參數出現時,爲了讓 Kotlin 的 API 在 Java 中工作,對於協變定義的 Box
我們生成 Box<Super>
作爲 Box<? extends Super>
(或者對於逆變定義的 Foo
生成 Foo<? super Bar>
)。當它是一個返回值時,
我們不生成通配符,因爲否則 Java 客戶端將必須處理它們(並且它違反常用
Java 編碼風格)。因此,我們的示例中的對應函數實際上翻譯如下:
// 作爲返回類型——沒有通配符
Box<Derived> boxDerived(Derived value) { …… }
// 作爲參數——有通配符
Base unboxBase(Box<? extends Base> box) { …… }
當參數類型是 final 時,生成通配符通常沒有意義,所以無論在什麼地方 Box<String>
始終轉換爲 Box<String>
。
{:.note}
如果我們在默認不生成通配符的地方需要通配符,我們可以使用 @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
不僅可用於單個類型參數,還可用於整個聲明(如函數或類),從而抑制其中的所有通配符。
{:.note}
Nothing
類型翻譯
類型 Nothing
是特殊的,因爲它在 Java 中沒有自然的對應。確實,每個 Java 引用類型,包括
java.lang.Void
都可以接受 null
值,但是 Nothing 不行。因此,這種類型不能在 Java 世界中準確表示。這就是爲什麼在使用 Nothing
參數的地方 Kotlin 生成一個原始類型:
fun emptyList(): List<Nothing> = listOf()
// 會翻譯成
// List emptyList() { …… }