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官网