前序
在Java專案中,多多少少都存在以Utils結尾的Java類。其內部並無任何狀態和例項函式,只有一堆與該名稱相關的靜態屬性或靜態方法。該類只是作為一種容器儲存著靜態屬性和靜態方法。
頂層函式
Kotlin認為,根本不需要建立這些無意義的類。可以直接將函式放在程式碼檔案的頂層,不用附屬於任何一個類。
在com.daqi包中的daqi.kt檔案中定義頂層函式joinToString()
package com.daqi
@JvmOverloads
fun <T> joinToString(collection: Collection<T>,
separator:String = ",",
prefix:String = "",
postfix:String = ""):String{
val result = StringBuilder(prefix)
for ((index,element) in collection.withIndex()){
if (index > 0)
result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
複製程式碼
在Kotlin中,頂層函式屬於包內成員,包內可以直接使用,包外只需要import該頂層函式,即可使用。
Kotlin和Java具有很強互操作性,如果讓Java呼叫頂層函式該怎麼呼叫呢?先看一下頂層函式編譯成Java是什麼樣的:
public final class DaqiKt {
@NotNull
public static final String joinToString(Collection collection, String separator,
String prefix,String postfix) {
}
}
複製程式碼
編譯器將頂層函式所在的檔名daqi.kt作為類名DaqiKt,生成對應的類檔案。該kt檔案下的所有頂層函式都編譯為這個類的靜態函式。
so,如果在Java中呼叫Kotlin的頂層函式時,需要對其的檔名轉換為對應的類名,再進行呼叫。
DaqiKt.joinToString(new ArrayList<>(),"",",","");
複製程式碼
如果想規定kt檔案轉換為Java類時的類名,可以使用@file:JvmName()註解進行修改。將其放在檔案的開頭,位於包名之前:
@file:JvmName("StringUtils")
package com.daqi
fun <T> joinToString(...){
...
}
複製程式碼
就可以使用特定的類名在Java中呼叫對應的頂層函式。
StringUtils.joinToString(new ArrayList<>(),"",",","");
複製程式碼
頂層屬性
既然有頂層方法,應該也有頂層屬性。和頂層函式一樣,屬性也可以放在檔案的頂層,不附屬與任何一個類。這種屬性叫頂層屬性。
@file:JvmName("StringUtils")
package com.daqi
val daqiField :String = "daqi"
複製程式碼
頂層屬性和其他任意屬性一樣,都提供對應的訪問器(val 變數提供getter,var 變數提供getter 和 setter)。也就是說,當Java訪問該頂層屬性時,通過訪問器進行訪問的。
StringUtils.getDaqiField();
複製程式碼
通過反編譯檢視其轉換為Java的樣子:
@NotNull
private static final String daqiField = "daqi";
@NotNull
public static final String getDaqiField() {
return daqiField;
}
複製程式碼
頂層屬性被定義為私有的靜態物件,並配套了一個靜態訪問器方法。
如果需要定義public的靜態變數,可以用const關鍵字修飾該變數。(僅適用於基礎資料型別和String型別的屬性)
在反編譯的檔案中可以看到,靜態屬性變成public,且沒有了具體的靜態訪問器。
//Kotlin
const val daqiField :String = "daqi"
//Java
public static final String daqiField = "daqi";
複製程式碼
擴充套件函式
Kotlin可以在無需繼承的情況下擴充套件一個類的功能,然後像內部函式一樣直接通過物件進行呼叫。擴充套件函式這個特性可以很平滑與現有Java程式碼進行整合。
宣告一個擴充套件函式,需要用一個接收者型別也就是被擴充套件的型別來作為他的字首。而呼叫該擴充套件函式的物件,叫作接收者物件。接收者物件用this表示,this可有可無。
fun String.lastChar():Char{
return this.get(this.length - 1)
}
//呼叫擴充套件函式
"daqi".lastChar()
複製程式碼
擴充套件函式的可見性
在擴充套件函式中,可以直接訪問被擴充套件類的方法和屬性。但擴充套件函式不允許你打破物件的封裝性,擴充套件函式不能訪問private和 protected的成員。具體什麼意思呢,先定義一個java類:
public class daqiJava {
private String str = "";
public String name = "";
public void daqi(){
}
private void daqi(String name){
}
}
複製程式碼
對其該類進行擴充套件:
public的方法可以正常訪問,但凡用private 或 protected修飾的屬性或方法,無法在擴充套件函式中被呼叫。
Java呼叫擴充套件函式
回到Kotlin和Java互動性的問題,Java如何呼叫擴充套件函式的呢?這時候又要一波反編譯:
public static final void extensionMethod(daqiJava $receiver,String string) {
}
複製程式碼
擴充套件函式daqiJava#extensionMethod()被轉換為一個相同名稱的靜態函式。函式第一個引數變成接受者型別,後面才是原函式的引數列表。也就是說Java呼叫擴充套件函式時,需要先傳入對應的接收者物件,再傳入該擴充套件函式的引數。
擴充套件是靜態解析的
在JVM語言的多型中,被重寫方法的呼叫依據其呼叫物件的實際型別進行呼叫。但擴充套件函式是靜態分發的,即意味著擴充套件函式是由其所在表示式中的呼叫者的型別來決定的。
我們都知道Button是View的子類,同時為View和Button定義名為daqi的擴充套件函式。
val view:View = Button()
view.daqi()
複製程式碼
此時呼叫的是View的擴充套件函式,即使它實質是一個Button物件。因為擴充套件函式所在的表示式中,view是View型別,而不是Button型別。
擴充套件函式其他特性
-
擴充套件函式與頂層函式類似,在Java層進行呼叫時,依據其所在的檔名作為類名,其作為靜態函式,儲存在該類中。(也支援@file:JvmName("")進行)
-
在擴充套件函式中,除了可以呼叫接收者型別的成員函式和成員屬性外,還可以呼叫該類的擴充套件函式。
-
如果一個類的成員函式與擴充套件函式擁有相同的方法簽名,成員函式會被優先使用。
-
擴充套件函式其實是靜態函式,擴充套件函式不能被子類重寫。但子類仍可以呼叫父類的擴充套件函式。
擴充套件屬性
擴充套件屬性不能有初始化器,它們的行為只能由顯式提供的 getters/setters 定義。因為沒有地方對它進行儲存,不可能給現有的Java物件例項新增額外的屬性。只是用屬性的語法對接受者型別進行擴充套件。
宣告一個擴充套件常量:
val String.lastChar:Char
get() = get(length - 1)
複製程式碼
宣告一個擴充套件變數:
var StringBuffer.lastChar:Char
get() = get(length - 1)
set(value:Char){
this.setCharAt(length - 1,value)
}
複製程式碼
總結:
- 頂層函式和擴充套件函式都可以去掉以”Utils“結尾的靜態方法容器類。
- 頂層函式和頂層屬性提供全域性的方法和屬性,不需要任何物件例項進行呼叫。
- 擴充套件函式需要接受者物件例項來進行呼叫。
- 頂層屬性是等價於私有的靜態物件。
- 若想獲取基本型別和String型別的公有靜態物件,在頂層屬性定義時,新增const關鍵字。
- 擴充套件可以毫無副作用給原有庫的類增加屬性和方法。
參考資料:
- 《Kotlin實戰》
- Kotlin官網