Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

抽象类

抽象类与普通类类似,只是其中一个或多个函数或属性是不完整的:函数缺少定义,属性没有初始化。接口类似于抽象类,但没有状态

您必须使用 abstract 修饰符来标记具有缺失定义的类成员。包含 abstract 函数或属性的类也必须标记为 abstract。尝试删除下面任何一个 abstract 修饰符,看看您会得到什么样的消息:

// Abstract/AbstractKeyword.kt
package abstractclasses

abstract class WithProperty {
  abstract val x: Int
}

abstract class WithFunctions {
  abstract fun f(): Int
  abstract fun g(n: Double)
}

WithProperty 使用没有初始化值的方式 声明x(一个 声明 描述了一个东西,但没有提供 定义 来创建存储值的空间或为函数提供代码)。如果没有初始化器,Kotlin 要求引用必须是 abstract,并且期望在类上添加 abstract 修饰符。没有初始化器,Kotlin 无法推断出类型,因此还需要对 abstract 引用提供类型信息。

WithFunctions 声明了 f()g(),但没有提供函数定义,同样强制您向函数和包含类中添加 abstract 修饰符。如果不为函数提供返回类型,就像 g() 那样,Kotlin 会假定它返回 Unit

抽象函数和属性必须以某种方式在您从抽象类创建的类中存在(变得具体)。

在接口中声明的所有函数和属性默认都是抽象的,这使得接口类似于抽象类。当接口包含函数或属性声明时,abstract 修饰符是多余的,可以删除。这两个接口是等价的:

// Abstract/Redundant.kt
package abstractclasses

interface Redundant {
  abstract val x: Int
  abstract fun f(): Int
  abstract fun g(n: Double)
}

interface Removed {
  val x: Int
  fun f(): Int
  fun g(n: Double)
}

接口和抽象类的区别在于,抽象类可以包含状态,而接口不能。状态是存储在属性内的数据。在以下示例中,IntList 的状态包括存储在属性 namelist 中的值。

// Abstract/StateOfAClass.kt
package abstractstate
import atomictest.eq

class IntList(val name: String) {
  val list = mutableListOf<Int>()
}

fun main() {
  val ints = IntList("numbers")
  ints.name eq "numbers"
  ints.list += 7
  ints.list eq listOf(7)
}

接口可以声明属性,但实际的数据仅存储在实现接口的类中。接口不允许在其属性中存储值:

// Abstract/NoStateInInterfaces.kt
package abstractclasses

interface IntList {
  val name: String
  // 编译不通过:
  // val list = listOf(0)
}

接口和抽象类都可以包含具有实现的函数。您可以从这些函数中调用其他 abstract 成员:

// Abstract/Implementations.kt
package abstractclasses
import atomictest.eq

interface Parent {
  val ch: Char
  fun f(): Int
  fun g() = "ch = $ch; f() = ${f()}"
}

class Actual(
  override val ch: Char        // [1]
): Parent {
  override fun f() = 17        // [2]
}

class Other : Parent {
  override val ch: Char        // [3]
    get() = 'B'
  override fun f() = 34        // [4]
}

fun main() {
  Actual('A').g() eq "ch = A; f() = 17" // [5]
  Other().g() eq "ch = B; f() = 34"     // [6]
}

Parent 声明了一个抽象属性 ch 和一个抽象函数 f(),这些在任何实现类中都必须被重写。行 [1]-[4] 展示了在子类中对这些成员的不同实现。

Parent.g() 在定义 g() 时没有定义的抽象成员。接口和抽象类保证在创建任何对象之前,所有抽象属性和函数都得到实现,而且您不能调用一个成员函数,除非您有一个对象。行 [5][6] 调用了不同实现的 chf()

因为接口可以包含函数实现,所以它也可以包含自定义的属性访问器,如果对应的属性不改变状态:

// Abstract/PropertyAccessor.kt
package abstractclasses
import atomictest.eq

interface PropertyAccessor {
  val a: Int
    get() = 11
}

class Impl : PropertyAccessor

fun main() {
  Impl().a eq 11
}

您可能会想知道为什么我们需要接口,当抽象类更强大时。为了理解“没有状态的类”的重要性,让我们来看一下 Kotlin 不支持的多重

继承概念。在 Kotlin 中,一个类只能从一个基类继承:

// Abstract/NoMultipleInheritance.kt
package multipleinheritance1

open class Animal
open class Mammal : Animal()
open class AquaticAnimal : Animal()

// 多个基类不能编译通过:
// class Dolphin : Mammal(), AquaticAnimal()

尝试编译注释掉的代码会产生错误:在 supertype 列表中只能出现一个类

Java 也是这样工作的。最初的 Java 设计者认为 C++ 的多重继承是一个坏主意。当时的主要复杂性和不满来自于多个状态继承。管理多个状态继承的规则很复杂,很容易引起混淆和令人惊讶的行为。Java 通过引入接口来解决了这个问题,接口不能包含状态。Java 禁止了多个状态继承,但允许多个接口继承,Kotlin 遵循了这个设计:

// Abstract/MultipleInterfaceInheritance.kt
package multipleinheritance2

interface Animal
interface Mammal: Animal
interface AquaticAnimal: Animal

class Dolphin : Mammal, AquaticAnimal

请注意,与类一样,接口也可以彼此继承。

在从多个接口继承时,可以同时覆盖具有相同签名的两个或更多函数(名称与参数和返回类型结合在一起)。如果函数或属性签名冲突,您必须手动解决冲突,如 class C 中所示:

// Abstract/InterfaceCollision.kt
package collision
import atomictest.eq

interface A {
  fun f() = 1
  fun g() = "A.g"
  val n: Double
    get() = 1.1
}

interface B {
  fun f() = 2
  fun g() = "B.g"
  val n: Double
    get() = 2.2
}

class C : A, B {
  override fun f() = 0
  override fun g() = super<A>.g()
  override val n: Double
    get() = super<A>.n + super<B>.n
}

fun main() {
  val c = C()
  c.f() eq 0
  c.g() eq "A.g"
  c.n eq 3.3
}

函数 f()g() 以及属性 n 在接口 AB 中具有相同的签名,因此 Kotlin 不知道如何处理,并且如果您不解决此问题,会产生错误消息(尝试逐个注释掉 C 中的定义)。成员函数和属性可以像 f() 中那样通过新定义进行覆盖,但函数也可以使用 super 关键字访问它们的基本版本,使用尖括号指定基类,如 C.g()C.n 的定义中所示。

标识符相同但类型不同的冲突在 Kotlin 中是不允许的,也不能解决。

练习和解答可以在 www.AtomicKotlin.com 找到。