[关闭]
@cxm-2016 2016-11-25T16:28:39.000000Z 字数 4786 阅读 2565

Kotlin(七)——类与继承

Kotlin

版本:1
翻译:李颖


Kotlin 中的类使用 class 关键字定义:

  1. class Invoice {
  2. }

类的定义由以下几部分组成: 类名, 类头部(指定类的类型参数, 主构造器, 等等.), 以及由大括号括起的类主体部分. 类的头部和主体部分都是可选的; 如果类没有主体部分, 那么大括号也可以省略.

  1. class Empty

构造器

Kotlin 中的类可以有一个 主构造器 (primary constructor), 以及一个或多个 次构造器 (secondary constructor). 主构造器是类头部的一部分, 位于类名称(以及可选的类型参数)之后.

  1. class Person constructor(firstName: String) {
  2. }

如果主构造器没有任何注解(annotation), 也没有任何可见度修饰符, 那么 constructor 关键字可以省略:

  1. class Person(firstName: String) {
  2. }

主构造器中不能包含任何代码. 初始化代码可以放在 初始化代码段 (initializer block) 中, 初始化代码段使用 init 关键字作为前缀:

  1. class Customer(name: String) {
  2. init {
  3. logger.info("Customer initialized with value ${name}")
  4. }
  5. }

注意, 主构造器的参数可以在初始化代码段中使用. 也可以在类主体定义的属性初始化代码中使用:

  1. class Customer(name: String) {
  2. val customerKey = name.toUpperCase()
  3. }

实际上, Kotlin 有一种简洁语法, 可以通过主构造器来定义属性并初始化属性值:

  1. class Person(val firstName: String, val lastName: String, var age: Int) {
  2. // ...
  3. }

与通常的属性一样, 主构造器中定义的属性可以是可变的(var), 也可以是只读的(val).

如果构造器有注解, 或者有可见度修饰符, 这时 constructor 关键字是必须的, 注解和修饰符要放在它之前:

  1. class Customer public @Inject constructor(name: String) { ... }

次级构造器(secondary constructor)

类还可以声明 次级构造器 (secondary constructor), 使用 constructor 关键字作为前缀:

  1. class Person {
  2. constructor(parent: Person) {
  3. parent.children.add(this)
  4. }
  5. }

如果类有主构造器, 那么每个次级构造器都必须委托给主构造器, 要么直接委托, 要么通过其他次级构造器间接委托. 委托到同一个类的另一个构造器时, 使用 this 关键字实现:

  1. class Person(val name: String) {
  2. constructor(name: String, parent: Person) : this(name) {
  3. parent.children.add(this)
  4. }
  5. }

如果一个非抽象类没有声明任何主构造器和次级构造器, 它将带有一个自动生成的, 无参数的主构造器. 这个构造器的可见度为 public. 如果不希望你的类带有 public 的构造器, 你需要声明一个空的构造器, 并明确设置其可见度:

  1. class DontCreateMe private constructor () {
  2. }

注意: 在 JVM 中, 如果主构造器的所有参数都指定了默认值, 编译器将会产生一个额外的无参数构造器, 这个无参数构造器会使用默认参数值来调用既有的构造器. 有些库(比如 Jackson 或 JPA) 会使用无参数构造器来创建对象实例, 这个特性将使得 Kotlin 比较容易与这种库协同工作.

  1. class Customer(val customerName: String = "")

创建类的实例

要创建一个类的实例, 我们需要调用类的构造器, 调用方式与使用通常的函数一样:

  1. val invoice = Invoice()
  2. val customer = Customer("Joe Smith")

注意, Kotlin 没有 new 关键字.

类成员

类中可以包含以下内容:

继承

Kotlin 中所有的类都有一个共同的超类 Any, 如果类声明时没有指定超类, 则默认为 Any:

  1. class Example // 隐含地继承自 Any

Any 不是 java.lang.Object; 尤其要注意, 除 equals(), hashCode()toString() 之外, 它没有任何成员.

要明确声明类的超类, 我们在类的头部添加一个冒号, 冒号之后指定超类:

  1. open class Base(p: Int)
  2. class Derived(p: Int) : Base(p)

如果类有主构造器, 那么可以(而且必须)在主构造器中使用主构造器的参数来初始化基类.

如果类没有主构造器, 那么所有的次级构造器都必须使用 super 关键字来初始化基类, 或者委托到另一个构造器, 由被委托的构造器来初始化基类. 注意, 这种情况下, 不同的次级构造器可以调用基类中不同的构造器:

  1. class MyView : View {
  2. constructor(ctx: Context) : super(ctx) {
  3. }
  4. constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
  5. }
  6. }

类上的 open 注解(annotation) 与 Java 的 final 正好相反: 这个注解表示允许从这个类继承出其他子类. 默认情况下, Kotlin 中所有的类都是 final 的, 这种设计符合 Effective Java, 一书中的第 17 条原则: 允许继承的地方, 应该明确设计, 并通过文档注明, 否则应该禁止继承.

成员的覆盖

我们在前面提到过, 我们很注意让 Kotlin 中的一切都明白无误. 而且与 Java 不同, Kotlin 要求明确地注解来标识允许被子类覆盖的成员(我们称之为 open), 而且也要求明确地注解来标识对超类成员的覆盖:

  1. open class Base {
  2. open fun v() {}
  3. fun nv() {}
  4. }
  5. class Derived() : Base() {
  6. override fun v() {}
  7. }

对于 Derived.v() 必须添加 override 注解. 如果遗漏了这个注解, 编译器将会报告错误. 如果一个函数没有标注 open 注解, 比如上例中的 Base.nv(), 那么在子类中声明一个同名同参的方法将是非法的, 无论是否添加 override 注解, 都不可以. 在一个 final 类(比如, 一个没有添加 open 注解的类)中, 声明 open 成员是禁止的.

当一个子类成员标记了 override 注解来覆盖父类成员时, 覆盖后的子类成员本身也将是 open 的, 也就是说, 子类成员可以被自己的子类再次覆盖. 如果你希望禁止这种再次覆盖, 可以使用 final 关键字:

  1. open class AnotherDerived() : Base() {
  2. final override fun v() {}
  3. }

属性的覆盖方式与方法覆盖类似. 注意, 你可以在主构造器的属性声明中使用 override 关键字:

  1. open class Foo {
  2. open val x: Int get { ... }
  3. }
  4. class Bar1(override val x: Int) : Foo() {
  5. }

你也可以使用一个 var 属性覆盖一个 val 属性, 但不可以反过来使用一个 val 属性覆盖一个 var 属性. 允许这种覆盖的原因是, val 属性本质上只是定义了一个 get 方法, 使用 var 属性来覆盖它, 只是向后代类中添加了一个 set 方法.

覆盖的规则

在 Kotlin 中, 类继承中的方法实现问题, 遵守以下规则: 如果一个类从它的直接超类中继承了同一个成员的多个实现, 那么这个子类必须覆盖这个成员, 并提供一个自己的实现(可以使用继承得到的多个实现中的某一个). 为了表示使用的方法是从哪个超类继承得到的, 我们使用 super 关键字, 将超类名称放在尖括号类, 比如, super<Base>:

  1. open class A {
  2. open fun f() { print("A") }
  3. fun a() { print("a") }
  4. }
  5. interface B {
  6. fun f() { print("B") } // 接口的成员默认是 'open' 的
  7. fun b() { print("b") }
  8. }
  9. class C() : A(), B {
  10. // 编译器要求 f() 方法必须覆盖:
  11. override fun f() {
  12. super<A>.f() // 调用 A.f()
  13. super<B>.f() // 调用 B.f()
  14. }
  15. }

同时继承 AB 是合法的, 而且函数 a()b() 的继承也不存在问题, 因为对于这两个函数, C 类都只继承得到了唯一的一个实现. 但对函数 f() 的继承就发生了问题, 因为 C 类从超类中继承得到了两个实现, 因此在 C 类中我们必须覆盖函数 f(), 并提供我们自己的实现, 这样才能消除歧义.

抽象类

类本身, 或类中的部分成员, 都可以声明为 abstract 的. 抽象成员在类中不存在具体的实现. 注意, 我们不必对抽象类或抽象成员标注 open 注解 – 因为它显然必须是 open 的.

我们可以使用抽象成员来覆盖一个非抽象的 open 成员:

  1. open class Base {
  2. open fun f() {}
  3. }
  4. abstract class Derived : Base() {
  5. override abstract fun f()
  6. }

同伴对象(Companion Object)

与 Java 或 C# 不同, Kotlin 的类没有静态方法(static method). 大多数情况下, 建议使用包级函数(package-level function)替代静态方法.

如果你需要写一个函数, 希望使用者不必通过类的实例来调用它, 但又需要访问类的内部信息(比如, 一个工厂方法), 你可以将这个函数写为这个类之内的一个 对象声明 的成员, 而不是类本身的成员.

具体来说, 如果你在类中声明一个 同伴对象, 那么只需要使用类名作为限定符就可以调用同伴对象的成员了, 语法与 Java/C# 中调用类的静态方法一样.

封闭类(Sealed Class)

封闭类(Sealed class)用来表示对类阶层的限制, 可以限定一个值只允许是某些指定的类型之一, 而不允许是其他类型. 感觉上, 封闭类是枚举类(enum class)的一种扩展: 枚举类的值也是有限的, 但每一个枚举值常数都只存在唯一的一个实例, 封闭类则不同, 它允许的子类类型是有限的, 但子类可以有多个实例, 每个实例都可以包含它自己的状态数据.

要声明一个封闭类, 需要将 sealed 修饰符放在类名之前. 封闭类可以有子类, 但所有的子类声明都必须嵌套在封闭类的声明部分之内.

  1. sealed class Expr {
  2. class Const(val number: Double) : Expr()
  3. class Sum(val e1: Expr, val e2: Expr) : Expr()
  4. object NotANumber : Expr()
  5. }

注意, 从封闭类的子类再继承的子类(间接继承者)可以放在任何地方, 不必在封闭类的声明部分之内.

使用封闭类的主要好处在于, 当使用 when expression 时, 可以验证分支语句覆盖了所有的可能情况, 因此就不必通过 else 分支来处理例外情况.

  1. fun eval(expr: Expr): Double = when(expr) {
  2. is Expr.Const -> expr.number
  3. is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
  4. Expr.NotANumber -> Double.NaN
  5. // 不需要 `else` 分支, 因为我们已经覆盖了所有的可能情况
  6. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注