抽象类
抽象类与普通类类似,只是其中一个或多个函数或属性是不完整的:函数缺少定义,属性没有初始化。接口类似于抽象类,但没有状态。
您必须使用 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
的状态包括存储在属性 name
和 list
中的值。
// 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] 调用了不同实现的 ch
和 f()
。
因为接口可以包含函数实现,所以它也可以包含自定义的属性访问器,如果对应的属性不改变状态:
// 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
在接口 A
和 B
中具有相同的签名,因此 Kotlin 不知道如何处理,并且如果您不解决此问题,会产生错误消息(尝试逐个注释掉 C
中的定义)。成员函数和属性可以像 f()
中那样通过新定义进行覆盖,但函数也可以使用 super
关键字访问它们的基本版本,使用尖括号指定基类,如 C.g()
和 C.n
的定义中所示。
标识符相同但类型不同的冲突在 Kotlin 中是不允许的,也不能解决。
练习和解答可以在 www.AtomicKotlin.com 找到。