前序
Kotlin的類和介面與Java的類和介面存在較大區別,本次主要歸納Kotlin的介面和類如何定義、繼承以及其一些具體細節,同時檢視其對應的Java層實現。
帶預設方法的介面
Kotlin介面可以包含抽象方法以及非抽象方法的實現(類似Java 8的預設方法)
interface MyInterface {
//抽象方法
fun daqi()
//非抽象方法(即提供預設實現方法)
fun defaultMethod() {
}
}
複製程式碼
介面也可以定義屬性。宣告的屬性可以是抽象的,也可以是提供具體訪問器實現的(即不算抽象的)。
interface MyInterface {
//抽象屬性
var length:Int
//提供訪問器的屬性
val name:String
get() = ""
//抽象方法
fun daqi()
//非抽象方法(即提供預設實現方法)
fun defaultMethod() {
}
}
複製程式碼
介面中宣告的屬性不能有幕後欄位。因為介面是無狀態的,因此介面中宣告的訪問器不能引用它們。(簡單說就是介面沒有具體的屬性,不能用幕後欄位對屬性進行賦值)
介面的實現
Kotlin使用 : 替代Java中的extends 和 implements 關鍵字。Kotlin和Java一樣,一個類可以實現任意多個介面,但是隻能繼承一個類。
介面中抽象的方法和抽象屬性,實現介面的類必須對其提供具體的實現。
對於在介面中提供預設實現的介面方法和提供具體訪問器的屬性,可以對其進行覆蓋,重新實現方法和提供新的訪問器實現。
class MyClass:MyInterface{
//原抽象屬性,提供具體訪問器
//不提供具體訪問器,提供初始值,使用預設訪問器也是沒有問題的
override var length: Int = 0
/*override var length: Int
get() = 0
set(value) {}*/
//覆蓋提供好訪問器的介面屬性
override val name: String
//super.name 其實是呼叫介面中定義的訪問器
get() = super.name
//原抽象方法,提供具體實現
override fun daqi() {
}
//覆蓋預設方法
override fun defaultMethod() {
super.defaultMethod()
}
}
複製程式碼
無論是從介面中獲取的屬性還是方法,前面都帶有一個override關鍵字。該關鍵字與Java的@Override註解類似,重寫父類或介面的方法屬性時,都 強制 需要用override修飾符進行修飾。因為這樣可以避免先寫出實現方法,再新增抽象方法造成的意外重寫。
介面的繼承
介面也可以從其他介面中派生出來,從而既提供基類成員的實現,也可以宣告新的方法和屬性。
interface Name {
val name:String
}
interface Person :Name{
fun learn()
}
class daqi:Person{
//為父介面的屬性提供具體的訪問器
override val name: String
get() = "daqi"
//為子介面的方法提供具體的實現
override fun learn() {
}
}
複製程式碼
覆蓋衝突
在C++中,存在菱形繼承的問題,即一個類同時繼承具有相同函式簽名的兩個方法,到底該選擇哪一個實現呢?由於Kotlin的介面支援預設方法,當一個類實現多個介面,同時擁有兩個具有相同函式簽名的預設方法時,到底選擇哪一個實現呢?
主要根據以下3條規則進行判斷:
1、類中帶override修飾的方法優先順序最高。 類或者父類中帶override修飾的方法的優先順序高於任何宣告為預設方法的優先順序。(Kotlin編譯器強制要求,當類中存在和父類或實現的介面有相同函式簽名的方法存在時,需要在前面新增override關鍵字修飾。)
2、當第一條無法判斷時,子介面的優先順序更高。優先選擇擁有最具體實現的預設方法的介面,因為從繼承角度理解,可以認為子介面的預設方法覆蓋重寫了父介面的預設方法,子介面比父介面具體。
3、最後還是無法判斷時,繼承多個介面的類需要顯示覆蓋重寫該方法,並選擇呼叫期望的預設方法。
- 如何理解第二條規則?先看看一下例子:
Java繼承自Language,兩者都對use方法提供了預設實現。而Java比Language更具體。
interface Language{
fun use() = println("使用語言")
}
interface Java:Language{
override fun use() = println("使用Java語言程式設計")
}
複製程式碼
而實現這兩個介面的類中,並無覆蓋重寫該方法,只能選擇更具體的預設方法作為其方法實現。
class Person:Java,Language{
}
//執行結果是輸出:使用Java語言程式設計
val daqi = Person()
daqi.use()
複製程式碼
- 如何理解第三條規則?繼續看例子:
介面Java和Kotlin都提供對learn方法提供了具體的預設實現,且兩者並無明確的繼承關係。
interface Java {
fun learn() = println("學習Java")
}
interface Kotlin{
fun learn() = println("學習Kotlin")
}
複製程式碼
當某類都實現Java和Kotlin介面時,此時就會產生覆蓋衝突的問題,這個時候編譯器會強制要求你提供自己的實現:
唯一的解決辦法就是顯示覆蓋該方法,如果想沿用介面的預設實現,可以super關鍵字,並將具體的介面名放在super的尖括號中進行呼叫。
class Person:Java,Kotlin{
override fun learn() {
super<Java>.learn()
super<Kotlin>.learn()
}
}
複製程式碼
對比Java 8的介面
Java 8中也一樣可以為介面提供預設實現,但需要使用default關鍵字進行標識。(Kotlin只需要提供具體的方法實現,即提供函式體)
public interface Java8 {
default void defaultMethod() {
System.out.println("我是Java8的預設方法");
}
}
複製程式碼
面對覆蓋衝突,Java8的和處理和Kotlin的基本相似,在語法上顯示呼叫介面的預設方法時有些不同:
//Java8 顯示呼叫覆蓋衝突的方法
Java8.super.defaultMethod()
//Kotlin 顯示呼叫覆蓋衝突的方法
super<Kotlin>.learn()
複製程式碼
Kotlin 與 Java 間介面的互動
眾所周知,Java8之前介面沒有預設方法,Kotlin是如何相容的呢?定義如下兩個介面,再檢視看一下反編譯的結果:
interface Language{
//預設方法
fun use() = println("使用語言程式設計")
}
interface Java:Language{
//抽象屬性
var className:String
//提供訪問器的屬性
val field:String
get() = ""
//預設方法
override fun use() = println("使用Java語言程式設計")
//抽象方法
fun absMethod()
}
複製程式碼
先檢視父介面的原始碼:
public interface Language {
void use();
public static final class DefaultImpls {
public static void use(Language $this) {
String var1 = "使用語言程式設計";
System.out.println(var1);
}
}
}
複製程式碼
Language介面中的預設方法轉換為抽象方法保留在介面中。其內部定義了一個名為DefaultImpls的靜態內部類,該內部類中擁有和預設方法相同名稱的靜態方法,而該靜態方法的實現就是其同名預設函式的具體實現。也就是說,Kotlin的預設方法轉換為靜態內部類DefaultImpls的同名靜態函式。
所以,如果想在Java中呼叫Kotlin介面的預設方法,需要加多一層DefaultImpls
public class daqiJava implements Language {
@Override
public void use() {
Language.DefaultImpls.use(this);
}
}
複製程式碼
再繼續檢視子介面的原始碼
public interface Java extends Language {
//抽象屬性的訪問器
@NotNull
String getClassName();
void setClassName(@NotNull String var1);
//提供具體訪問器的屬性
@NotNull
String getField();
//預設方法
void use();
//抽象方法
void absMethod();
public static final class DefaultImpls {
@NotNull
public static String getField(Java $this) {
return "";
}
public static void use(Java $this) {
String var1 = "使用Java語言程式設計";
System.out.println(var1);
}
}
}
複製程式碼
通過原始碼觀察到,無論是抽象屬性還是擁有具體訪問器的屬性,都沒有在介面中定義任何屬性,只是宣告瞭對應的訪問器方法。(和擴充套件屬性相似)
抽象屬性和提供具體訪問器的屬性區別是:
- 抽象屬性的訪問器均為抽象方法。
- 擁有具體訪問器的屬性,其訪問器實現和預設方法一樣,外部宣告一個同名抽象方法,具體實現被儲存在靜態內部類DefaultImpls的同名靜態函式中。
Java定義的介面,Kotlin繼承後能為其父介面的方法提供預設實現嗎?當然是可以啦:
//Java介面
public interface daqiInterface {
String name = "";
void absMethod();
}
//Kotlin介面
interface daqi: daqiInterface {
override fun absMethod() {
}
}
複製程式碼
Java介面中定義的屬性都是預設public static final,對於Java的靜態屬性,在Kotlin中可以像頂層屬性一樣,直接對其進行使用:
fun main(args: Array<String>) {
println("Java介面中的靜態屬性name = $name")
}
複製程式碼
類
Kotlin的類可以有一個主建構函式以及一個或多個 從建構函式。主建構函式是類頭的一部分,即在類體外部宣告。
主構造方法
constructor關鍵字可以用來宣告 主構造方法 或 從構造方法。
class Person(val name:String)
//其等價於
class Person constructor(val name:String)
複製程式碼
主建構函式不能包含任何的程式碼。初始化的程式碼可以放到以 init 關鍵字作為字首的初始化塊中。
class Person constructor(val name:String){
init {
println("name = $name")
}
}
複製程式碼
構造方法的引數也可以設定為預設引數,當所有構造方法的引數都是預設引數時,編譯器會生成一個額外的不帶引數的構造方法來使用所有的預設值。
class Person constructor(val name:String = "daqi"){
init {
println("name = $name")
}
}
//輸出為:name = daqi
fun main(args: Array<String>) {
Person()
}
複製程式碼
主構造方法同時需要初始化父類,子類可以在其列表引數中索取父類構造方法所需的引數,以便為父類構造方法提供引數。
open class Person constructor(name:String){
}
class daqi(name:String):Person(name){
}
複製程式碼
當沒有給一個類宣告任何構造方法,編譯器將生成一個不做任何事情的預設構造方法。對於只有預設構造方法的類,其子類必須顯式地呼叫父類的預設構造方法,即使他沒有引數。
open class View
class Button:View()
複製程式碼
而介面沒有構造方法,所以介面名後不加括號。
//實現介面
class Button:ClickListener
複製程式碼
當 主構造方法 有註解或可見性修飾符時,constructor 關鍵字不可忽略,並且constructor 在這些修飾符和註解的後面。
class Person public @Inject constructor(val name:String)
複製程式碼
構造方法的可見性是 public,如果想將構造方法設定為私有,可以使用private修飾符。
class Person private constructor()
複製程式碼
從構造方法
從構造方法使用constructor關鍵字進行宣告
open class View{
//從構造方法1
constructor(context:Context){
}
//從構造方法2
constructor(context:Context,attr:AttributeSet){
}
}
複製程式碼
使用this關鍵字,從一個構造方法中呼叫該類另一個構造方法,同時也能使用super()關鍵字呼叫父類構造方法。
如果一個類有 主構造方法,每個 從構造方法 都應該顯式呼叫 主構造方法,否則將其委派給會呼叫主構造方法的從構造方法。
class Person constructor(){
//從構造方法1,顯式呼叫主構造方法
constructor(string: String) : this() {
println("從構造方法1")
}
//從構造方法2,顯式呼叫構造方法1,間接呼叫主構造方法。
constructor(data: Int) : this("daqi") {
println("從構造方法2")
}
}
複製程式碼
注意:
初始化塊中的程式碼實際上會成為主建構函式的一部分。顯式呼叫主構造方法會作為次建構函式的第一條語句,因此所有初始化塊中的程式碼都會在次建構函式體之前執行。
即使該類沒有主建構函式,這種呼叫仍會隱式發生,並且仍會執行初始化塊。
//沒有主構造方法的類
class Person{
init {
println("主構造方法 init 1")
}
//從構造方法預設會執行所有初始化塊
constructor(string: String) {
println("從構造方法1")
}
init {
println("主構造方法 init 2")
}
}
複製程式碼
如果一個類擁有父類,但沒有主構造方法時,每個從構造方法都應該初始化父類(即呼叫父類的構造方法),否則將其委託給會初始化父類的構造方法(即使用this呼叫其他會初始化父類的構造方法)。
class MyButton:View{
//呼叫自身的另外一個從構造方法,間接呼叫父類的構造方法。
constructor(context:Context):this(context,MY_STYLE){
}
//呼叫父類的構造方法,初始化父類。
constructor(context:Context,attr:AttributeSet):super(context,attr){
}
}
複製程式碼
脆弱的基類
Java中允許建立任意類的子類並重寫任意方法,除非顯式地使用final關鍵字。對基類進行修改導致子類不正確的行為,就是所謂的脆弱的基類。所以Kotlin中類和方法預設是final,Java類和方法預設是open的。
當你允許一個類存在子類時,需要使用open修飾符修改這個類。如果想一個方法能被子類重寫,也需要使用open修飾符修飾。
open class Person{
//該方法時final 子類不能對它進行重寫
fun getName(){}
//子類可以對其進行重寫
open fun getAge(){}
}
複製程式碼
對基類或介面的成員進行重寫後,重寫的成員同樣預設為open。(儘管其為override修飾)
如果想改變重寫成員預設為open的行為,可以顯式的將重寫成員標註為final
open class daqi:Person(){
final override fun getAge() {
super.getAge()
}
}
複製程式碼
抽象類的成員和介面的成員始終是open的,不需要顯式地使用open修飾符。
可見性修飾符
Kotlin和Java的可見性修飾符相似,同樣可以使用public、protected和private修飾符。但Kotlin預設可見性是public,而Java預設可見性是包私有。
Kotlin中並沒有包私有這種可見性,Kotlin提供了一個新的修飾符:internal,表示“只在模組內部可見”。模組是指一組一起編譯的Kotlin檔案。可能是一個Gradle專案,可能是一個Idea模組。internal可見性的優勢在於它提供了對模組實現細節的封裝。
Kotlin允許在頂層宣告中使用private修飾符,其中包括類宣告,方法宣告和屬性宣告,但這些宣告只能在宣告它們的檔案中可見。
注意:
- 覆蓋一個 protected 成員並且沒有顯式指定其可見性,該成員的可見性還是 protected 。
- 與Java不同,Kotlin的外部類(巢狀類)不能看到其內部類中的private成員。
- internal修飾符編譯成位元組碼轉Java後,會變成public。
- private類轉換為Java時,會變成包私有宣告,因為Java中類不能宣告為private。
內部類和巢狀類
Kotlin像Java一樣,允許在一個類中宣告另一個類。但Kotlin的巢狀類預設不能訪問外部類的例項,和Java的靜態內部類一樣。
如果想讓Kotlin內部類像Java內部類一樣,持有一個外部類的引用的話,需要使用inner修飾符。
內部類需要外部類引用時,需要使用 this@外部類名 來獲取。
class Person{
private val name = "daqi"
inner class MyInner{
fun getPersonInfo(){
println("name = ${this@Person.name}")
}
}
}
複製程式碼
object關鍵字
物件宣告
在Java中建立單例往往需要定義一個private的構造方法,並建立一個靜態屬性來持有這個類的單例。
Kotlin通過物件宣告將類宣告和類的單一例項結合在一起。物件宣告在定義的時候就立即建立,而這個初始化過程是執行緒安全的。
物件宣告中可以包含屬性、方法、初始化語句等,也支援繼承類和實現介面,唯一不允許的是不能定義構造方法(包括主構造方法和從構造方法)。
物件宣告不能定義在方法和內部類中,但可以定義在其他的物件宣告和非內部類(例如:巢狀類)。如果需要引用該物件,直接使用其名稱即可。
//定義物件宣告
class Book private constructor(val name:String){
object Factory {
val name = "印書廠"
fun createAppleBooK():Book{
return Book("Apple")
}
fun createAndroidBooK():Book{
return Book("Android")
}
}
}
複製程式碼
呼叫物件宣告的屬性和方法:
Book.Factory.name
Book.Factory.createAndroidBooK()
複製程式碼
將物件宣告反編譯成Java程式碼,其內部實現也是定義一個private的構造方法,並始終建立一個名為INSTANCE的靜態屬性來持有這個類的單例,而該類的初始化放在靜態程式碼塊中。
public final class Book {
//....
public Book(String name, DefaultConstructorMarker $constructor_marker) {
this(name);
}
public static final class Factory {
@NotNull
private static final String name = "印書廠";
public static final Book.Factory INSTANCE;
//...
@NotNull
public final Book createAppleBooK() {
return new Book("Apple", (DefaultConstructorMarker)null);
}
@NotNull
public final Book createAndroidBooK() {
return new Book("Android", (DefaultConstructorMarker)null);
}
private Factory() {
}
static {
Book.Factory var0 = new Book.Factory();
INSTANCE = var0;
name = "印書廠";
}
}
}
複製程式碼
用Java呼叫物件宣告的方法:
//Java呼叫物件宣告
Book.Factory.INSTANCE.createAndroidBooK();
複製程式碼
伴生物件
一般情況下,使用頂層函式可以很好的替代Java中的靜態函式,但頂層函式無法訪問類的private成員。
當需要定義一個方法,該方法能在沒有類例項的情況下,呼叫該類的內部方法。可以定義一個該類的物件宣告,並在該物件宣告中定義該方法。類內部的物件宣告可以用 companion 關鍵字標記,這種物件叫伴生物件。
可以直接通過類名來訪問該伴生物件的方法和屬性,不用再顯式的指明物件宣告的名稱,再訪問該物件宣告物件的方法和屬性。可以像呼叫該類的靜態函式和屬性一樣,不需要再關心物件宣告的名稱。
//將構造方法私有化
class Book private constructor(val name:String){
//伴生物件的名稱可定義也可以不定義。
companion object {
//伴生物件呼叫其內部私有構造方法
fun createAppleBooK():Book{
return Book("Apple")
}
fun createAndroidBooK():Book{
return Book("Android")
}
}
}
複製程式碼
呼叫伴生物件的方法:
Book.createAndroidBooK()
複製程式碼
伴生物件的實現和物件宣告類似,定義一個private的構造方法,並始終建立一個名為Companion的靜態屬性來持有這個類的單例,並直接對Companion靜態屬性進行初始化。
public final class Book {
//..
public static final Book.Companion Companion = new Book.Companion((DefaultConstructorMarker)null);
//...
public static final class Companion {
//...
@NotNull
public final Book createAppleBooK() {
return new Book("Apple", (DefaultConstructorMarker)null);
}
@NotNull
public final Book createAndroidBooK() {
return new Book("Android", (DefaultConstructorMarker)null);
}
private Companion() {
}
// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
複製程式碼
伴生物件的擴充套件
擴充套件方法機制允許在任何地方定義某類的擴充套件方法,但需要該類的例項進行呼叫。當需要擴充套件一個通過類自身呼叫的方法時,如果該類擁有伴生物件,可以通過對伴生物件定義擴充套件方法。
//對伴生物件定義擴充套件方法
fun Book.Companion.sellBooks(){
}
複製程式碼
當對該擴充套件方法進行呼叫時,可以直接通過類自身進行呼叫:
Book.sellBooks()
複製程式碼
匿名內部類
作為android開發者,在設定監聽時,建立匿名物件的情況再常見不過了。
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
複製程式碼
object關鍵字除了能用來宣告單例式物件外,還可以宣告匿名物件。和物件宣告不同,匿名物件不是單例,每次都會建立一個新的物件例項。
mRecyclerView.setOnClickListener(object :View.OnClickListener{
override fun onClick(v: View?) {
}
});
複製程式碼
當該匿名類擁有兩個以上抽象方法時,才需要使用object建立匿名類。否則儘量使用lambda表示式。
mButton.setOnClickListener {
}
複製程式碼
參考資料:
- 《Kotlin實戰》
- Kotlin官網