Kotlin和Java的雙向互操作
本文收錄于: https://github.com/mengdd/KotlinTutorials
Kotlin和Java是有互操作性的(Interoperability). Kotlin和Java代碼可以互相調(diào)用.
為什么在一個(gè)項(xiàng)目里這兩種語言會(huì)同時(shí)存在呢?
- 改造Java項(xiàng)目遷移到Kotlin時(shí), 漸進(jìn)改動(dòng)就會(huì)有兩種語言同時(shí)存在, 相互調(diào)用的情況.
- 即便你選擇了一種語言, 很可能也需要用到庫是用另一種語言寫的. 比如新寫一個(gè)Kotlin項(xiàng)目, 但是用到的庫仍然是Java的.
Kotlin調(diào)用Java
空安全
因?yàn)镴ava中的所有引用都是可能為null的. Java聲明的類型在Kotlin中被稱為platform types.
從Java中傳過來的引用, 可以賦值給Kotlin的非空類型, 不會(huì)有編譯錯(cuò)誤, 但是如果引用值為空, 會(huì)在運(yùn)行時(shí)拋出異常.
舉例, 在kotlin中調(diào)用Java的方法, 返回一個(gè)String:
val stringOne = JavaUtils.getStringOne()
println(stringOne)
println(stringOne.length)
如果Java方法返回null怎么辦?
運(yùn)行這段程序時(shí)先打出null, 再拋出NullPointerException.
如果程序是這樣寫的:
val stringOne: String = JavaUtils.getStringOne()
println(stringOne)
println(stringOne.length)
說明Kotlin假設(shè)Java傳回來的是一個(gè)非空值. 這段代碼編譯時(shí)不會(huì)報(bào)錯(cuò), 但是運(yùn)行時(shí)第一行就拋出IllegalStateException.
如果這樣寫:
val stringOne: String? = JavaUtils.getStringOne()
println(stringOne)
println(stringOne?.length)
終于利用上了Kotlin的空安全檢查, 所有用到這個(gè)變量的地方都要加上?, 如果不做檢查編譯時(shí)就會(huì)提示錯(cuò)誤.
但是這樣防御難免導(dǎo)致代碼太啰嗦了, 可能到處都是?.
好的實(shí)踐是Java中的Public APIs(Non-primitive parameters, Field type, Return)都應(yīng)該加上注解.
如果Java的類型上有關(guān)于null的注解, 就會(huì)直接表示為Kotlin中為不為null或者可為null的對(duì)應(yīng)類型.
注解可以來自于各種包中, 比如JetBrains提供的: @Nullable和@NotNull.
比如:
@NotNull
public static String getStringOne() {
return "hello";
}
這樣Kotlin代碼就知道傳過來的肯定是個(gè)非空值, 可以放心使用.
如果是@Nullable, 編譯器就會(huì)提示使用前做檢查.
轉(zhuǎn)義在Kotlin中作為關(guān)鍵字的Java標(biāo)識(shí)符
Kotlin中的關(guān)鍵字, 比如:
fun, in, is, object, typealias, typeof, val, var, when
如果Java代碼中用了這些關(guān)鍵字, 在Kotlin中調(diào)用該Java代碼就要用`進(jìn)行轉(zhuǎn)義.
比如如果java中有一個(gè)名稱為is的方法, 在kotlin中想要調(diào)用:
foo.`is`(bar)
但是, 首先需要考慮是不是名字起得不好, 如果可以改名(不是第三方代碼), 優(yōu)先考慮改名.
比較常見的一個(gè)使用情形是在寫測(cè)試的時(shí)候, Mockito中的when就需要轉(zhuǎn)義:
Mockito.`when`(xxx.foo()).thenReturn(yyy)
因?yàn)镸ockito是一個(gè)Java的第三方庫, 我們沒法改它.
另一個(gè)解決辦法是使用import alias, 給這個(gè)方法取個(gè)別名:
import org.mockito.Mockito.`when` as whenever
這樣在使用的時(shí)候就可以用whenever來代替了when了.
import alias通常用來解決命名沖突的問題.
SAM Conversions
SAM: Single Abstract Method.
只要函數(shù)參數(shù)匹配, Kotlin的函數(shù)可以自動(dòng)轉(zhuǎn)換為Java的接口實(shí)現(xiàn).
Convention: 可以做SAM轉(zhuǎn)換的參數(shù)類型應(yīng)該放在方法的最后, 這樣看起來更舒服.
舉例, 如果在Java中定義方法:
interface Operation {
int doCalculate(int left, int right);
}
public static int calculate(Operation operation, int firstNumber, int secondNumber) {
return operation.doCalculate(firstNumber, secondNumber);
}
在Kotlin中調(diào)用的時(shí)候用SAM轉(zhuǎn)換, 用一個(gè)lambda作為接口實(shí)現(xiàn):
JavaUtils.calculate({ number1, number2 -> number1 + number2 }, 2, 3)
這樣雖然正確, 但是可以改進(jìn).
把Java方法定義中的參數(shù)位置交換一下, 把接口參數(shù)放在最后:
public static int calculate(int firstNumber, int secondNumber, Operation operation) {
return operation.doCalculate(firstNumber, secondNumber);
}
在Kotlin中, 最后一個(gè)lambda參數(shù)可以提取到括號(hào)外面:
JavaUtils.calculate(2, 3) { number1, number2 -> number1 + number2 }
這樣看起來更好.
注意: SAM conversion只應(yīng)用于java interop.
上面的例子, 如果接口和方法是在Kotlin中定義的:
interface Operation2 {
fun doCalculate(left: Int, right: Int): Int
}
fun calculate2(firstNumber: Int, secondNumber: Int, operation: Operation2): Int {
return operation.doCalculate(firstNumber, secondNumber)
}
SAM conversions就不能用了, IDE會(huì)提示無法識(shí)別.
調(diào)用這個(gè)方法時(shí), 第三個(gè)參數(shù)必須寫成這種(匿名類的對(duì)象, 實(shí)現(xiàn)了接口):
calculate2(2, 3, object : Operation2 {
override fun doCalculate(left: Int, right: Int): Int {
return left + right
}
})
這是因?yàn)樵贙otlin的世界里, 函數(shù)是第一公民.
如果把前面的方法參數(shù)改為function type:
fun calculate3(firstNumber: Int, secondNumber: Int, operation: (Int, Int) -> Int): Int {
return operation.invoke(firstNumber, secondNumber)
}
就可以像之前SAM conversions似的使用:
calculate3(2, 3) { number1, number2 -> number1 + number2 }
如果接口是在Java中定義, 但是接收參數(shù)的方法是Kotlin的方法:
fun calculate4(firstNumber: Int, secondNumber: Int, operation: JavaUtils.Operation): Int {
return operation.doCalculate(firstNumber, secondNumber)
}
仍然是不能用SAM conversions, 因?yàn)檫@個(gè)方法仍然是可以接受函數(shù)類型的參數(shù)的.
在Kotlin中調(diào)用:
calculate4(2, 3, object : JavaUtils.Operation {
override fun doCalculate(left: Int, right: Int): Int {
return left + right
}
})
IDE會(huì)提示你簡(jiǎn)化為:
calculate4(2, 3, JavaUtils.Operation { left, right -> left + right })
注意這里接口名稱不能省略.
是不是感覺有點(diǎn)暈, 我把上面提到的幾個(gè)調(diào)用情況寫在一起:
// java function, java interface parameter
private fun trySAM1() {
JavaUtils.calculate(2, 3) { number1, number2 -> number1 + number2 }
}
// kotlin function, kotlin interface parameter
private fun trySAM2() {
calculate2(2, 3, object : Operation2 {
override fun doCalculate(left: Int, right: Int): Int {
return left + right
}
})
}
// kotlin function, function type parameter
private fun trySAM3() {
calculate3(2, 3) { number1, number2 -> number1 + number2 }
}
// kotlin function, java interface parameter
private fun trySAM4() {
calculate4(2, 3, JavaUtils.Operation { left, right -> left + right })
}
可以互相比較一下, 看看區(qū)別.
Getter和Setter
Java中的getter和setter在Kotlin中會(huì)表現(xiàn)為properties. 但是如果只有setter, 不會(huì)作為可見的property.
異常
Kotlin中所有的異常都是unchecked的, 所以如果調(diào)用的Java代碼有受檢異常, kotlin并不會(huì)強(qiáng)迫你處理.
其他
Java中返回void的方法在Kotlin中會(huì)變成Unit.
java.lang.Object會(huì)變成Any, Any中的很多方法都是擴(kuò)展方法.
Java沒有運(yùn)算符重載, 但是Kotlin支持. (運(yùn)算符重載容易存在過度使用的問題.)
Java調(diào)用Kotlin
屬性
Kotlin的屬性會(huì)被編譯成Java中的一個(gè)私有字段, 加上getter和setter方法.
如果想要作為一個(gè)字段, 可以加上@JvmField注解.
包級(jí)別的方法
如果在一個(gè)文件app.kt中定義方法, 包名是org.example, 會(huì)被編譯成Java的靜態(tài)方法, Java類的類名是org.example.AppKt.
應(yīng)用場(chǎng)景舉例: 舊代碼中有一個(gè)Java的輔助類, 包含靜態(tài)方法:
public class Utils {
public static int distanceBetween(int point1, int point2) {
return point2 - point1;
}
}
要把這個(gè)輔助類遷移到Kotlin代碼, 可以新建一個(gè)Kotlin文件DistanceUtils.kt, 直接寫包級(jí)別的方法:
fun distanceBetween(point1: Int, point2: Int): Int {
return point2 - point1
}
在Java中調(diào)用這個(gè)方法的時(shí)候:
DistanceUtilsKt.distanceBetween(7, 9);
如果原先的Java代碼中包含調(diào)用這個(gè)方法的地方太多, 又不想改所有的usage, 怎么辦? ->
可以通過注解@file:JvmmName("xxx")改變類名. 這樣原先Java代碼中調(diào)用的地方就避免了修改.
如果有兩個(gè)文件指定了相同的JvmName, 編譯會(huì)報(bào)錯(cuò). 可以通過加上@file:JvmMultifileClass來解決. 這樣多個(gè)Kotlin文件中定義的輔助方法對(duì)于Java來說會(huì)統(tǒng)一到同一個(gè)類中.
實(shí)例字段
如你需要把kotlin的property作為字段暴露出來, 可以加上@JvmField注解.
適用的property: 有backing field, 沒有這些修飾符: private, open, override, const, 也不是代理屬性.
lateinit的屬性會(huì)自動(dòng)暴露為fields, 可見性和屬性的setter一致.
Kotlin的data class會(huì)自動(dòng)生成getter/setter. 如果加上@JvmField, 會(huì)直接暴露這些字段.
可以通過@get:JvmName("xxx")和@set:JvmName("xxx")來定制getter和setter的名字.
靜態(tài)字段
Kotlin在有名字的object或者companion object中聲明的屬性, 將會(huì)編譯成靜態(tài)字段.
通常這些字段是private的, 不過也可以通過以下幾種方式暴露:
-
@JvmField. -
lateinit. -
const.
靜態(tài)方法
前面提過, Kotlin包級(jí)別的方法會(huì)被編譯成靜態(tài)方法.
在object或companion object中聲明的方法, 默認(rèn)是類中的實(shí)例方法. 比如:
class StaticMethodsDemoClass {
companion object {
fun sayHello() {
println("hello")
}
}
}
object SingletonObject {
fun sayWorld() {
println("world")
}
}
在Java中調(diào)用的時(shí)候:
StaticMethodsDemoClass.Companion.sayHello();
SingletonObject.INSTANCE.sayWorld();
如果給object或companion object中的方法加上@JvmStatic, 會(huì)生成一個(gè)靜態(tài)方法和一個(gè)實(shí)例方法.
調(diào)用的時(shí)候就可以省略掉中間的Companion或INSTANCE關(guān)鍵字, 以類名直接調(diào)用靜態(tài)方法.
比如:
class StaticMethodsDemoClass {
companion object {
fun sayHello() {
println("hello")
}
@JvmStatic
fun sayHelloStatic() {
println("hello")
}
}
}
調(diào)用的時(shí)候:
StaticMethodsDemoClass.Companion.sayHello();
//StaticMethodsDemoClass.sayHello(); // error
StaticMethodsDemoClass.Companion.sayHelloStatic(); // ok, but not necessary
StaticMethodsDemoClass.sayHelloStatic();
@JvmStatic也可以用于屬性, 就會(huì)有靜態(tài)版本的getter和setter方法.
其他
Kotlin方法支持默認(rèn)參數(shù), 在Java中只有全部參數(shù)的方法簽名才是可見的. 如果你希望對(duì)Java暴露多個(gè)方法重載, 要給方法加上@JvmOverloads.
比如在Kotlin中寫一個(gè)自定義View的構(gòu)造函數(shù):
class DialView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}
還可以利用
@JvmName來給方法重命名. 因?yàn)樵贙otlin中是擴(kuò)展方法, 在Java中只是一個(gè)靜態(tài)方法, 名字可能不夠直觀.
Kotlin沒有checked exceptions.
如果想在Java中調(diào)用一個(gè)Kotlin方法, 并包一個(gè)try-catch, 會(huì)報(bào)錯(cuò)說沒有拋出這個(gè)異常.
可以在Kotlin方法中加上注解, 比如@Throws(IOException::class).
Feature leak prevention
Kotlin方法的參數(shù)名, 在生成代碼中會(huì)作為一個(gè)字符串出現(xiàn), 從而不會(huì)被混淆, 有可能會(huì)泄漏. 所以不建議放敏感信息到參數(shù)名中.
類似的還有字段名, 擴(kuò)展方法名.
可以在proguard中加上
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void checkParameterIsNotNull(...);
public static void throwUninitializedPropertyAccessException(...);
}
來移除這些代碼.
但是建議在測(cè)試環(huán)境中仍然保留這些代碼, 以便有錯(cuò)誤發(fā)生的時(shí)候能夠快速發(fā)現(xiàn).
Tools
IDE的自動(dòng)轉(zhuǎn)換
Code -> Convert Java File to Kotlin File.
如果粘貼Java代碼到.kt文件, IDE會(huì)自動(dòng)將所粘貼代碼轉(zhuǎn)換為Kotlin代碼.
查看編譯成的Java代碼
在IDE里面可以顯示Kotlin Bytecode, 然后decompile, 顯示java代碼.
Tools -> Kotlin -> Show Kotlin Bytecode -> Decompile.