????Kotlin的類和接口與Java的類和接口存在較大區(qū)別,本次主要?dú)w納Kotlin的接口和類如何定義、繼承以及其一些具體細(xì)節(jié),同時查看其對應(yīng)的Java層實(shí)現(xiàn)。
帶默認(rèn)方法的接口
????Kotlin接口可以包含抽象方法以及非抽象方法的實(shí)現(xiàn)(類似Java 8的默認(rèn)方法)
interface MyInterface {
//抽象方法
fun daqi()
//非抽象方法(即提供默認(rèn)實(shí)現(xiàn)方法)
fun defaultMethod() {
}
}
????接口也可以定義屬性。聲明的屬性可以是抽象的,也可以是提供具體訪問器實(shí)現(xiàn)的(即不算抽象的)。
interface MyInterface {
//抽象屬性
var length:Int
//提供訪問器的屬性
val name:String
get() = ""
//抽象方法
fun daqi()
//非抽象方法(即提供默認(rèn)實(shí)現(xiàn)方法)
fun defaultMethod() {
}
}
????接口中聲明的屬性不能有幕后字段。因?yàn)榻涌谑菬o狀態(tài)的,因此接口中聲明的訪問器不能引用它們。(簡單說就是接口沒有具體的屬性,不能用幕后字段對屬性進(jìn)行賦值)
接口的實(shí)現(xiàn)
????Kotlin使用 : 替代Java中的extends 和 implements 關(guān)鍵字。Kotlin和Java一樣,一個類可以實(shí)現(xiàn)任意多個接口,但是只能繼承一個類。
????接口中抽象的方法和抽象屬性,實(shí)現(xiàn)接口的類必須對其提供具體的實(shí)現(xiàn)。
????對于在接口中提供默認(rèn)實(shí)現(xiàn)的接口方法和提供具體訪問器的屬性,可以對其進(jìn)行覆蓋,重新實(shí)現(xiàn)方法和提供新的訪問器實(shí)現(xiàn)。
class MyClass:MyInterface{
//原抽象屬性,提供具體訪問器
//不提供具體訪問器,提供初始值,使用默認(rèn)訪問器也是沒有問題的
override var length: Int = 0
/*override var length: Int
get() = 0
set(value) {}*/
//覆蓋提供好訪問器的接口屬性
override val name: String
//super.name 其實(shí)是調(diào)用接口中定義的訪問器
get() = super.name
//原抽象方法,提供具體實(shí)現(xiàn)
override fun daqi() {
}
//覆蓋默認(rèn)方法
override fun defaultMethod() {
super.defaultMethod()
}
}
????無論是從接口中獲取的屬性還是方法,前面都帶有一個override關(guān)鍵字。該關(guān)鍵字與Java的@Override注解類似,重寫父類或接口的方法屬性時,都 強(qiáng)制 需要用override修飾符進(jìn)行修飾。因?yàn)檫@樣可以避免先寫出實(shí)現(xiàn)方法,再添加抽象方法造成的意外重寫。
接口的繼承
????接口也可以從其他接口中派生出來,從而既提供基類成員的實(shí)現(xiàn),也可以聲明新的方法和屬性。
interface Name {
val name:String
}
interface Person :Name{
fun learn()
}
class daqi:Person{
//為父接口的屬性提供具體的訪問器
override val name: String
get() = "daqi"
//為子接口的方法提供具體的實(shí)現(xiàn)
override fun learn() {
}
}
覆蓋沖突
????在C++中,存在菱形繼承的問題,即一個類同時繼承具有相同函數(shù)簽名的兩個方法,到底該選擇哪一個實(shí)現(xiàn)呢?由于Kotlin的接口支持默認(rèn)方法,當(dāng)一個類實(shí)現(xiàn)多個接口,同時擁有兩個具有相同函數(shù)簽名的默認(rèn)方法時,到底選擇哪一個實(shí)現(xiàn)呢?
主要根據(jù)以下3條規(guī)則進(jìn)行判斷:
????1、類中帶override修飾的方法優(yōu)先級最高。 類或者父類中帶override修飾的方法的優(yōu)先級高于任何聲明為默認(rèn)方法的優(yōu)先級。(Kotlin編譯器強(qiáng)制要求,當(dāng)類中存在和父類或?qū)崿F(xiàn)的接口有相同函數(shù)簽名的方法存在時,需要在前面添加override關(guān)鍵字修飾。)
????2、當(dāng)?shù)谝粭l無法判斷時,子接口的優(yōu)先級更高。優(yōu)先選擇擁有最具體實(shí)現(xiàn)的默認(rèn)方法的接口,因?yàn)閺睦^承角度理解,可以認(rèn)為子接口的默認(rèn)方法覆蓋重寫了父接口的默認(rèn)方法,子接口比父接口具體。
????3、最后還是無法判斷時,繼承多個接口的類需要顯示覆蓋重寫該方法,并選擇調(diào)用期望的默認(rèn)方法。
- 如何理解第二條規(guī)則?先看看一下例子:
Java繼承自Language,兩者都對use方法提供了默認(rèn)實(shí)現(xiàn)。而Java比Language更具體。
interface Language{
fun use() = println("使用語言")
}
interface Java:Language{
override fun use() = println("使用Java語言編程")
}
而實(shí)現(xiàn)這兩個接口的類中,并無覆蓋重寫該方法,只能選擇更具體的默認(rèn)方法作為其方法實(shí)現(xiàn)。
class Person:Java,Language{
}
//執(zhí)行結(jié)果是輸出:使用Java語言編程
val daqi = Person()
daqi.use()
- 如何理解第三條規(guī)則?繼續(xù)看例子:
接口Java和Kotlin都提供對learn方法提供了具體的默認(rèn)實(shí)現(xiàn),且兩者并無明確的繼承關(guān)系。
interface Java {
fun learn() = println("學(xué)習(xí)Java")
}
interface Kotlin{
fun learn() = println("學(xué)習(xí)Kotlin")
}
當(dāng)某類都實(shí)現(xiàn)Java和Kotlin接口時,此時就會產(chǎn)生覆蓋沖突的問題,這個時候編譯器會強(qiáng)制要求你提供自己的實(shí)現(xiàn):
唯一的解決辦法就是顯示覆蓋該方法,如果想沿用接口的默認(rèn)實(shí)現(xiàn),可以super關(guān)鍵字,并將具體的接口名放在super的尖括號中進(jìn)行調(diào)用。
class Person:Java,Kotlin{
override fun learn() {
super<Java>.learn()
super<Kotlin>.learn()
}
}
對比Java 8的接口
????Java 8中也一樣可以為接口提供默認(rèn)實(shí)現(xiàn),但需要使用default關(guān)鍵字進(jìn)行標(biāo)識。(Kotlin只需要提供具體的方法實(shí)現(xiàn),即提供函數(shù)體)
public interface Java8 {
default void defaultMethod() {
System.out.println("我是Java8的默認(rèn)方法");
}
}
????面對覆蓋沖突,Java8的和處理和Kotlin的基本相似,在語法上顯示調(diào)用接口的默認(rèn)方法時有些不同:
//Java8 顯示調(diào)用覆蓋沖突的方法
Java8.super.defaultMethod()
//Kotlin 顯示調(diào)用覆蓋沖突的方法
super<Kotlin>.learn()
Kotlin 與 Java 間接口的交互
????眾所周知,Java8之前接口沒有默認(rèn)方法,Kotlin是如何兼容的呢?定義如下兩個接口,再查看看一下反編譯的結(jié)果:
interface Language{
//默認(rèn)方法
fun use() = println("使用語言編程")
}
interface Java:Language{
//抽象屬性
var className:String
//提供訪問器的屬性
val field:String
get() = ""
//默認(rèn)方法
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接口中的默認(rèn)方法轉(zhuǎn)換為抽象方法保留在接口中。其內(nèi)部定義了一個名為DefaultImpls的靜態(tài)內(nèi)部類,該內(nèi)部類中擁有和默認(rèn)方法相同名稱的靜態(tài)方法,而該靜態(tài)方法的實(shí)現(xiàn)就是其同名默認(rèn)函數(shù)的具體實(shí)現(xiàn)。也就是說,Kotlin的默認(rèn)方法轉(zhuǎn)換為靜態(tài)內(nèi)部類DefaultImpls的同名靜態(tài)函數(shù)。
所以,如果想在Java中調(diào)用Kotlin接口的默認(rèn)方法,需要加多一層DefaultImpls
public class daqiJava implements Language {
@Override
public void use() {
Language.DefaultImpls.use(this);
}
}
再繼續(xù)查看子接口的源碼
public interface Java extends Language {
//抽象屬性的訪問器
@NotNull
String getClassName();
void setClassName(@NotNull String var1);
//提供具體訪問器的屬性
@NotNull
String getField();
//默認(rèn)方法
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);
}
}
}
????通過源碼觀察到,無論是抽象屬性還是擁有具體訪問器的屬性,都沒有在接口中定義任何屬性,只是聲明了對應(yīng)的訪問器方法。(和擴(kuò)展屬性相似)
抽象屬性和提供具體訪問器的屬性區(qū)別是:
- 抽象屬性的訪問器均為抽象方法。
- 擁有具體訪問器的屬性,其訪問器實(shí)現(xiàn)和默認(rèn)方法一樣,外部聲明一個同名抽象方法,具體實(shí)現(xiàn)被存儲在靜態(tài)內(nèi)部類DefaultImpls的同名靜態(tài)函數(shù)中。
Java定義的接口,Kotlin繼承后能為其父接口的方法提供默認(rèn)實(shí)現(xiàn)嗎?當(dāng)然是可以啦:
//Java接口
public interface daqiInterface {
String name = "";
void absMethod();
}
//Kotlin接口
interface daqi: daqiInterface {
override fun absMethod() {
}
}
????Java接口中定義的屬性都是默認(rèn)public static final,對于Java的靜態(tài)屬性,在Kotlin中可以像頂層屬性一樣,直接對其進(jìn)行使用:
fun main(args: Array<String>) {
println("Java接口中的靜態(tài)屬性name = $name")
}
類
????Kotlin的類可以有一個主構(gòu)造函數(shù)以及一個或多個 從構(gòu)造函數(shù)。主構(gòu)造函數(shù)是類頭的一部分,即在類體外部聲明。
主構(gòu)造方法
constructor關(guān)鍵字可以用來聲明 主構(gòu)造方法 或 從構(gòu)造方法。
class Person(val name:String)
//其等價于
class Person constructor(val name:String)
????主構(gòu)造函數(shù)不能包含任何的代碼。初始化的代碼可以放到以 init 關(guān)鍵字作為前綴的初始化塊中。
class Person constructor(val name:String){
init {
println("name = $name")
}
}
????構(gòu)造方法的參數(shù)也可以設(shè)置為默認(rèn)參數(shù),當(dāng)所有構(gòu)造方法的參數(shù)都是默認(rèn)參數(shù)時,編譯器會生成一個額外的不帶參數(shù)的構(gòu)造方法來使用所有的默認(rèn)值。
class Person constructor(val name:String = "daqi"){
init {
println("name = $name")
}
}
//輸出為:name = daqi
fun main(args: Array<String>) {
Person()
}
????主構(gòu)造方法同時需要初始化父類,子類可以在其列表參數(shù)中索取父類構(gòu)造方法所需的參數(shù),以便為父類構(gòu)造方法提供參數(shù)。
open class Person constructor(name:String){
}
class daqi(name:String):Person(name){
}
????當(dāng)沒有給一個類聲明任何構(gòu)造方法,編譯器將生成一個不做任何事情的默認(rèn)構(gòu)造方法。對于只有默認(rèn)構(gòu)造方法的類,其子類必須顯式地調(diào)用父類的默認(rèn)構(gòu)造方法,即使他沒有參數(shù)。
open class View
class Button:View()
而接口沒有構(gòu)造方法,所以接口名后不加括號。
//實(shí)現(xiàn)接口
class Button:ClickListener
當(dāng) 主構(gòu)造方法 有注解或可見性修飾符時,constructor 關(guān)鍵字不可忽略,并且constructor 在這些修飾符和注解的后面。
class Person public @Inject constructor(val name:String)
構(gòu)造方法的可見性是 public,如果想將構(gòu)造方法設(shè)置為私有,可以使用private修飾符。
class Person private constructor()
從構(gòu)造方法
從構(gòu)造方法使用constructor關(guān)鍵字進(jìn)行聲明
open class View{
//從構(gòu)造方法1
constructor(context:Context){
}
//從構(gòu)造方法2
constructor(context:Context,attr:AttributeSet){
}
}
????使用this關(guān)鍵字,從一個構(gòu)造方法中調(diào)用該類另一個構(gòu)造方法,同時也能使用super()關(guān)鍵字調(diào)用父類構(gòu)造方法。
????如果一個類有 主構(gòu)造方法,每個 從構(gòu)造方法 都應(yīng)該顯式調(diào)用 主構(gòu)造方法,否則將其委派給會調(diào)用主構(gòu)造方法的從構(gòu)造方法。
class Person constructor(){
//從構(gòu)造方法1,顯式調(diào)用主構(gòu)造方法
constructor(string: String) : this() {
println("從構(gòu)造方法1")
}
//從構(gòu)造方法2,顯式調(diào)用構(gòu)造方法1,間接調(diào)用主構(gòu)造方法。
constructor(data: Int) : this("daqi") {
println("從構(gòu)造方法2")
}
}
注意:
????初始化塊中的代碼實(shí)際上會成為主構(gòu)造函數(shù)的一部分。顯式調(diào)用主構(gòu)造方法會作為次構(gòu)造函數(shù)的第一條語句,因此所有初始化塊中的代碼都會在次構(gòu)造函數(shù)體之前執(zhí)行。
即使該類沒有主構(gòu)造函數(shù),這種調(diào)用仍會隱式發(fā)生,并且仍會執(zhí)行初始化塊。
//沒有主構(gòu)造方法的類
class Person{
init {
println("主構(gòu)造方法 init 1")
}
//從構(gòu)造方法默認(rèn)會執(zhí)行所有初始化塊
constructor(string: String) {
println("從構(gòu)造方法1")
}
init {
println("主構(gòu)造方法 init 2")
}
}
????如果一個類擁有父類,但沒有主構(gòu)造方法時,每個從構(gòu)造方法都應(yīng)該初始化父類(即調(diào)用父類的構(gòu)造方法),否則將其委托給會初始化父類的構(gòu)造方法(即使用this調(diào)用其他會初始化父類的構(gòu)造方法)。
class MyButton:View{
//調(diào)用自身的另外一個從構(gòu)造方法,間接調(diào)用父類的構(gòu)造方法。
constructor(context:Context):this(context,MY_STYLE){
}
//調(diào)用父類的構(gòu)造方法,初始化父類。
constructor(context:Context,attr:AttributeSet):super(context,attr){
}
}
脆弱的基類
????Java中允許創(chuàng)建任意類的子類并重寫任意方法,除非顯式地使用final關(guān)鍵字。對基類進(jìn)行修改導(dǎo)致子類不正確的行為,就是所謂的脆弱的基類。所以Kotlin中類和方法默認(rèn)是final,Java類和方法默認(rèn)是open的。
????當(dāng)你允許一個類存在子類時,需要使用open修飾符修改這個類。如果想一個方法能被子類重寫,也需要使用open修飾符修飾。
open class Person{
//該方法時final 子類不能對它進(jìn)行重寫
fun getName(){}
//子類可以對其進(jìn)行重寫
open fun getAge(){}
}
對基類或接口的成員進(jìn)行重寫后,重寫的成員同樣默認(rèn)為open。(盡管其為override修飾)
如果想改變重寫成員默認(rèn)為open的行為,可以顯式的將重寫成員標(biāo)注為final
open class daqi:Person(){
final override fun getAge() {
super.getAge()
}
}
抽象類的成員和接口的成員始終是open的,不需要顯式地使用open修飾符。
可見性修飾符
????Kotlin和Java的可見性修飾符相似,同樣可以使用public、protected和private修飾符。但Kotlin默認(rèn)可見性是public,而Java默認(rèn)可見性是包私有。
????Kotlin中并沒有包私有這種可見性,Kotlin提供了一個新的修飾符:internal,表示“只在模塊內(nèi)部可見”。模塊是指一組一起編譯的Kotlin文件??赡苁且粋€Gradle項目,可能是一個Idea模塊。internal可見性的優(yōu)勢在于它提供了對模塊實(shí)現(xiàn)細(xì)節(jié)的封裝。
????Kotlin允許在頂層聲明中使用private修飾符,其中包括類聲明,方法聲明和屬性聲明,但這些聲明只能在聲明它們的文件中可見。
注意:
- 覆蓋一個 protected 成員并且沒有顯式指定其可見性,該成員的可見性還是 protected 。
- 與Java不同,Kotlin的外部類(嵌套類)不能看到其內(nèi)部類中的private成員。
- internal修飾符編譯成字節(jié)碼轉(zhuǎn)Java后,會變成public。
- private類轉(zhuǎn)換為Java時,會變成包私有聲明,因?yàn)镴ava中類不能聲明為private。
內(nèi)部類和嵌套類
????Kotlin像Java一樣,允許在一個類中聲明另一個類。但Kotlin的嵌套類默認(rèn)不能訪問外部類的實(shí)例,和Java的靜態(tài)內(nèi)部類一樣。
????如果想讓Kotlin內(nèi)部類像Java內(nèi)部類一樣,持有一個外部類的引用的話,需要使用inner修飾符。
內(nèi)部類需要外部類引用時,需要使用 this@外部類名 來獲取。
class Person{
private val name = "daqi"
inner class MyInner{
fun getPersonInfo(){
println("name = ${this@Person.name}")
}
}
}
object關(guān)鍵字
對象聲明
????在Java中創(chuàng)建單例往往需要定義一個private的構(gòu)造方法,并創(chuàng)建一個靜態(tài)屬性來持有這個類的單例。
????Kotlin通過對象聲明將類聲明和類的單一實(shí)例結(jié)合在一起。對象聲明在定義的時候就立即創(chuàng)建,而這個初始化過程是線程安全的。
????對象聲明中可以包含屬性、方法、初始化語句等,也支持繼承類和實(shí)現(xiàn)接口,唯一不允許的是不能定義構(gòu)造方法(包括主構(gòu)造方法和從構(gòu)造方法)。
????對象聲明不能定義在方法和內(nèi)部類中,但可以定義在其他的對象聲明和非內(nèi)部類(例如:嵌套類)。如果需要引用該對象,直接使用其名稱即可。
//定義對象聲明
class Book private constructor(val name:String){
object Factory {
val name = "印書廠"
fun createAppleBooK():Book{
return Book("Apple")
}
fun createAndroidBooK():Book{
return Book("Android")
}
}
}
調(diào)用對象聲明的屬性和方法:
Book.Factory.name
Book.Factory.createAndroidBooK()
????將對象聲明反編譯成Java代碼,其內(nèi)部實(shí)現(xiàn)也是定義一個private的構(gòu)造方法,并始終創(chuàng)建一個名為INSTANCE的靜態(tài)屬性來持有這個類的單例,而該類的初始化放在靜態(tài)代碼塊中。
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調(diào)用對象聲明的方法:
//Java調(diào)用對象聲明
Book.Factory.INSTANCE.createAndroidBooK();
伴生對象
????一般情況下,使用頂層函數(shù)可以很好的替代Java中的靜態(tài)函數(shù),但頂層函數(shù)無法訪問類的private成員。
????當(dāng)需要定義一個方法,該方法能在沒有類實(shí)例的情況下,調(diào)用該類的內(nèi)部方法??梢远x一個該類的對象聲明,并在該對象聲明中定義該方法。類內(nèi)部的對象聲明可以用 companion 關(guān)鍵字標(biāo)記,這種對象叫伴生對象。
????可以直接通過類名來訪問該伴生對象的方法和屬性,不用再顯式的指明對象聲明的名稱,再訪問該對象聲明對象的方法和屬性。可以像調(diào)用該類的靜態(tài)函數(shù)和屬性一樣,不需要再關(guān)心對象聲明的名稱。
//將構(gòu)造方法私有化
class Book private constructor(val name:String){
//伴生對象的名稱可定義也可以不定義。
companion object {
//伴生對象調(diào)用其內(nèi)部私有構(gòu)造方法
fun createAppleBooK():Book{
return Book("Apple")
}
fun createAndroidBooK():Book{
return Book("Android")
}
}
}
調(diào)用伴生對象的方法:
Book.createAndroidBooK()
????伴生對象的實(shí)現(xiàn)和對象聲明類似,定義一個private的構(gòu)造方法,并始終創(chuàng)建一個名為Companion的靜態(tài)屬性來持有這個類的單例,并直接對Companion靜態(tài)屬性進(jìn)行初始化。
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();
}
}
}
伴生對象的擴(kuò)展
????擴(kuò)展方法機(jī)制允許在任何地方定義某類的擴(kuò)展方法,但需要該類的實(shí)例進(jìn)行調(diào)用。當(dāng)需要擴(kuò)展一個通過類自身調(diào)用的方法時,如果該類擁有伴生對象,可以通過對伴生對象定義擴(kuò)展方法。
//對伴生對象定義擴(kuò)展方法
fun Book.Companion.sellBooks(){
}
當(dāng)對該擴(kuò)展方法進(jìn)行調(diào)用時,可以直接通過類自身進(jìn)行調(diào)用:
Book.sellBooks()
匿名內(nèi)部類
作為android開發(fā)者,在設(shè)置監(jiān)聽時,創(chuàng)建匿名對象的情況再常見不過了。
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
????object關(guān)鍵字除了能用來聲明單例式對象外,還可以聲明匿名對象。和對象聲明不同,匿名對象不是單例,每次都會創(chuàng)建一個新的對象實(shí)例。
mRecyclerView.setOnClickListener(object :View.OnClickListener{
override fun onClick(v: View?) {
}
});
????當(dāng)該匿名類擁有兩個以上抽象方法時,才需要使用object創(chuàng)建匿名類。否則盡量使用lambda表達(dá)式。
mButton.setOnClickListener {
}
參考文獻(xiàn):
- 《Kotlin實(shí)戰(zhàn)》
- Kotlin官網(wǎng)