Kotlin知识归纳(一) —— 基础语法
Kotlin知识归纳(二) —— 让函数更好调用
Kotlin知识归纳(三) —— 顶层成员与扩展
Kotlin知识归纳(四) —— 接口和类
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中也一样可以为接口提供默认实现,但需要使用default关键字进行标识。(Kotlin只需要提供具体的方法实现,即提供函数体)
public interface Java8 { default void defaultMethod() { System.out.println("我是Java8的默认方法"); } } 复制代码
面对覆盖冲突,Java8的和处理和Kotlin的基本相似,在语法上显示调用接口的默认方法时有些不同:
//Java8 显示调用覆盖冲突的方法 Java8.super.defaultMethod() //Kotlin 显示调用覆盖冲突的方法 super<Kotlin>.learn() 复制代码
众所周知,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); } } } 复制代码
通过源码观察到,无论是抽象属性还是拥有具体访问器的属性,都没有在接口中定义任何属性,只是声明了对应的访问器方法。(和扩展属性相似)
抽象属性和提供具体访问器的属性区别是:
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修饰符,其中包括类声明,方法声明和属性声明,但这些声明只能在声明它们的文件中可见。
Kotlin像Java一样,允许在一个类中声明另一个类。但Kotlin的嵌套类默认不能访问外部类的实例,和Java的静态内部类一样。
如果想让Kotlin内部类像Java内部类一样,持有一个外部类的引用的话,需要使用inner修饰符。
内部类需要外部类引用时,需要使用 this@外部类名 来获取。
class Person{ private val name = "daqi" inner class MyInner{ fun getPersonInfo(){ println("name = ${this@Person.name}") } } } 复制代码
在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 { } 复制代码