Kotlin 知识梳理(3) - 类、对象和接口

Kotlin 知识梳理系列文章

Kotlin 知识梳理(1) - Kotlin 基础
Kotlin 知识梳理(2) - 函数的定义与调用
Kotlin 知识梳理(3) - 类、对象和接口
Kotlin 知识梳理(4) - 数据类、类委托 及 object 关键字
Kotlin 知识梳理(5) - lambda 表达式和成员引用
Kotlin 知识梳理(6) - Kotlin 的可空性
Kotlin 知识梳理(7) - Kotlin 的类型系统
Kotlin 知识梳理(8) - 运算符重载及其他约定
Kotlin 知识梳理(9) - 委托属性
Kotlin 知识梳理(10) - 高阶函数:Lambda 作为形参或返回值
Kotlin 知识梳理(11) - 内联函数
Kotlin 知识梳理(12) - 泛型类型参数


一、本文概要

本文是对<<Kotlin in Action>>的学习笔记,如果需要运行相应的代码可以访问在线环境 try.kotlinlang.org,这部分的思维导图为:

二、定义类继承结构

2.1 Kotlin 中的接口

Kotlin的接口可以包含以下两种类型的方法:

  • 简单的抽象方法
  • 包含默认实现的抽象方法

简单接口

  • 一个简单的Kotlin接口使用 interface 关键字来声明,所有实现这个接口的非抽象类都需要实现接口中定义的抽象方法。
  • Kotlin在类名后面使用 冒号 代替了Java中的extendsimplements关键字,一个类可以实现多个接口,但是只能继承一个类。
  • override修饰符用来标注被重写的父类或者接口的方法和属性,并且是 强制要求 的。

下面的例子中定义了一个接口,并演示了如何实现该接口,以及接口中定义的抽象方法:


在接口中定义方法的默认实现

我们可以给接口的方法提供一个默认的实现,定义的方法和普通函数相同。



如果一个类实现了两个接口,而这两个接口定义了相同的方法,并且都提供了该方法的默认实现,那么该类必须显示实现该方法,否则会在编译时报错:


调用继承自接口的方法的实现

当需要调用一个继承的实现,可以使用与Java相同的关键字 super,并在后面的尖括号中指明父类的名字,最后是调用的方法名

2.2 访问性修饰符:open、final、abstract

一般类

  • Kotlin中,类和方法默认都是final的,如果想允许创建一个类的子类,需要使用open修饰符来标示这个类,此外还需要给每一个允许被重写的属性或方法添加open修饰符。
  • 如果重写了一个基类的成员,重写了的函数同样默认是open的,如果想改变这一行为,可以显示地将重写的成员标注为final

抽象类

我们可以将一个类声明为abstract,这种类不能被实例化,一个从抽象类通常包含一些没有实现并且必须在子类重写的抽象成员:

  • 抽象类中的抽象函数:没有函数体就默认是abstract的,不一定要加上关键字,其访问性始终是open的。
  • 抽象类中的非抽象函数:默认是final的,如果需要重写,那么需要加上open修饰符。

小结

openfinalabstract这三个访问修饰符都 只适用于类,不能用在接口 当中:

  • open:用于声明一个类可以被继承,或者方法可以被子类重写。
  • final:不允许类被继承,或者不允许方法被重写。
  • abstract:声明抽象类,或者抽象类中的抽象方法。

当我们需要重写方法时,必须加上override修饰符。

2.3 可见性修饰符

Kotlin的可见性修饰符包括以下四种:

修饰符 类成员 顶层声明
public 所有地方可见 所有地方可见
internal 模块中可见 模块中可见
protected 子类中可见 ---
private 类中可见 文件中可见

JavaKotlin在可见性上的区别包括以下几点:

  • Java中默认的可见性是包私有的,而在Kotlin中,默认的可见性是public的。Kotlininternal作为包可见的替代方案,它表示“只在模块内部可见”。
  • Kotlin允许在顶层声明中使用private可见性,包括类、函数和属性,这些声明就只在声明它们的文件中可见,这是隐藏子系统实现细节的非常有用的方式。
  • 类的扩展函数不能访问它的privateprotected成员。
  • Kotlin中,一个外部类不能看到其内部类中的private成员。

2.4 内部类和嵌套类

Kotlin中,如果我们像Java一样,在一个类的内部定义一个类,那么它并不是一个 内部类,而是 嵌套类,区别在于嵌套类不会持有外部类的引用,也就是说它实际上是一个静态内部类:

嵌套类

如果要把它嵌套类变成一个 内部类 来持有一个外部类的引用的话需要使用inner修饰符,并且访问外部类时,需要使用this@{外部类名}的方式。

内部类

2.5 密封类:定义受限的类继承结构

之前在介绍when表达式的时候,我们用了一个表达式的例子,NumSum继承于基类Expr,分别表达数字和两个表达式之和,而对于不属于Expr的子类,我们需要提供额外的else操作符。


假如我们给Expr添加了一个新的子类,编译器并不能发现有地方改变了。如果忘记了添加一个新分支,就会选择默认的选项,这有可能导致潜在的bug

Kotlin为这个问题提供了一个解决方案:sealed类。为父类添加一个sealed修饰符,对可能创建的子类做出严格的限制,所有的直接子类必须嵌套在父类中。之前的例子修改如下:


这时候,我们在when表达式中已经处理了所有Expr的子类,就不再需要提供默认的分支,假如这时候我们给Expr添加一个新的子类Multi,但是不修改when中的逻辑,那么就会导致编译失败:

提示的信息为:

在这种情况下,Expr类有一个只能在类内部调用的private构造方法,你也不能声明一个sealed接口,因为如果这样做,Kotlin编译器不能保证任何人都不能在Java代码中实现这个接口。

三、构造方法

Java中,一个类可以声明一个或多个构造方法,Kotlin则将构造方法分为两类:

  • 主构造方法:主要而简洁的初始化类的方法,并且在 类体外部声明
  • 从构造方法:在 类体内部声明

3.1 初始化类:主构造方法和初始化语句块

假设,我们需要定义一个包含只读nickname属性的User类,最简单的方式为:


上面这段被括号围起来的语句块就叫做 主构造方法,它有两个目的:

  • 表明构造方法的参数。
  • 定义使用这些参数初始化的属性,也就是nikename

用于完成上面这两个功能的最明确的代码如下所示:


  • constructor:用来开始一个主构造方法和从构造方法的声明。
  • init:引入一个初始化块语句,这种语句块包含了在类被创建时执行的代码,并会与主构造方法一起使用,因为主构造方法有语法限制,这就是为什么要使用初始化语句块的原因。

在上面的例子中有几个可以简化的点:

  • 放在初始化语句块的语句可以和nikename的声明结合,因此可以去掉init语句。
  • 如果主构造方法没有注解或可见性修饰符,可以取消constructor关键字。
  • 如果属性用相应的构造方法参数来初始化,代码可以通过把val关键字加在参数前的方式来进行简化。

经过了以上三点,就会得到最前面简化后的结果。

对于构造方法,也可以采用之前在 Kotlin 知识梳理(2) - 函数的定义与调用 中介绍的 命名参数默认参数值 的技巧,如果所有的构造方法都有默认值,编译器会生成一个额外的不带参数的构造方法来使用所有的默认值。

Java中,如果父类定义了一个构造方法,那么在子类的构造方法中,必须要通过super方法初始化父类,例如:

//父类。
public class User {

    private String nikeName;

    User(String nikeName) {
        this.nikeName = nikeName;
    }
}
//子类。
public class TwitterUser extends User {

    public TwitterUser(String nikeName) {
       super(nikeName);
    }
}

而在Kotlin中,可以通过在基类列表的父类引用中提供父类构造方法参数的方式来做到这一点:


假如一个类没有声明任何的构造方法,将会生成一个不做任何事的默认构造方法,如果有子类继承了它,那么必须显示地调用父类的构造方法,即使它没有任何的参数。
如果想要确保你的类不被其它代码实例化,必须把构造方法标记为private,我们对上面的例子进行修改:


报错的原因为:


在大多数真实的场景中,类的构造方法是非常简明的:它要么没有参数或者直接与参数对应的属性关联,这就是为了Kotlin有为主构造方法设计的简洁的语法。

3.2 用不同的方式来初始化父类

大多数在Java中需要重载构造方法的场景都被Kotlin支持命名参数和参数默认值的语法所覆盖了。

定义从构造方法:constructor

而当我们需要扩展一个框架来提供多个构造方法,以便于通过不同的方式来初始化类的时候,就会需要用到从构造方法,从构造方式使用constructor方法引出,例如下面的代码:


运行结果为:

子类调用父类的从构造方法:super

如果想要扩展这个类,可以声明同样的构造方法,并使用super关键字调用对应的父类构造方法:


运行结果为:

子类调用自己的另一个构造方法:this

如果想要从一个构造方法中,调用你自己的类的另一个构造方法,那么可以使用this关键字:


运行结果为:

需要注意:如果类没有主构造方法,那么每个从构造方法必须初始化基类(通过super关键字)或者委托给另一个这样做了的构造方法(通过this关键字),也就是说,每个从构造方法必须以一个朝外的箭头开始,并且结束于任意一个基类构造方法,就像上面例子中Button的带有两个参数的从构造方法所做的那样。

3.3 实现在接口中声明的属性

Kotlin中,接口可以包含抽象属性的声明:


但是接口并没有说明这个值应该存储到一个支持字段还是通过getter来获取,接口本身并不包含任何状态,因此只有实现这个接口的类在需要的时候会存储这个值。

下面是三个例子:


  • PrivateUser:直接在主构造方法中声明了这个属性,这个属性实现了来自于User的抽象属性,所以要标记为override
  • SubscribingUser:通过一个自定义的getter实现,这个属性没有一个支持字段来存储它的值,它只有一个getter在每次调用时从email中得到昵称。
  • FacebookUser:在初始化时,将nickname属性与值关联。

接口除了可以声明抽象属性外,还可以包含具有gettersetter的属性,只要它们没有引用一个支持字段(支持字段需要在接口中存储状态,而这是不允许的):

  • email:必须在子类中重写。
  • nickname:有一个自定义的getter,可以被子类继承。

运行结果为:


4.4 通过 getter 和 setter 访问支持字段

现在,我们已经学习了两种属性的用法:

  • 存储值的属性
  • 具有自定义访问器在每次访问时计算值的属性

现在,我们结合以上两种,来实现一个既可以存储值,又可以在值被访问和修改时提供额外逻辑的属性:


运行结果为:

上面的address就是 有支持字段的属性,它和 没有支持字段的属性 的区别在于:

  • 如果显示地引用或者使用默认的访问器实现,编译器会为属性生成支持字段。
  • 如果你提供了一个自定义的访问器实现并且没有使用field,支持字段就不会被呈现出来。

4.5 修改访问器的可见性

访问器的可见性默认与属性的可见性相同,但是如果需要可以通过在getset关键字前放置可见性修饰符的方式来修改它,例如在下面的例子中,我们将setter的可见性修改为private


运行结果为:


更多文章,欢迎访问我的 Android 知识梳理系列:

推薦閱讀更多精彩內容