类和继承
类
Kotlin 的类使用关键字 class 来定义:
class Invoice {}
类的声明包含:类名、类头(指明类型参数、首要构造器等)、类体(由花括号包裹)。类头和类体都是可选的;如果没有类体,花括号也可以省略。
class Empty
构造器
Kotlin 的一个类可以有一个首要构造器和多个次要构造器。首要构造器是类头的一部分:位于类名(以及可选类型参数)之后。
class Person constructor(firstName: String) {}
如果首要构造器没有任何标注或者可视化修饰符,constructor 关键字可以省略。
class Person(firstName: String) {}
首要构造器不能包含任何代码。初始化代码可位于用 init 关键字作为前缀的初始化区块(initializer block)中。
一个实例在初始化的过程中,初始化区块会按照他们声明的顺序执行,中间可以穿插属性的初始化。
class InitOrderDemo(name: String) {val firstProperty = "First property: $name".also(::println)init {println("First initializer block that prints ${name}")}val secondProperty = "Second property: ${name.length}".also(::println)init {println("Second initializer block that prints ${name.length}")}}
注意:首要构造器的参数可以直接用于初始化区块。当然也可以用于属性初始化。
class Customer(name: String) {val customer = name.toUpperCase();}
实际上,有简洁的语法支持在首要构造器中声明属性并对他们进行初始化:
class Person(val firstName: String, val lastName: String, val age: Int) {// ...}
和其他常规的属性一样,首要构造器中的属性可以被声明为可变(var)或者只读(val)。
如果构造器中有注解或者可见性修饰符,必须要有 constructor 关键字,修饰符位于它前面:
class Customer public @Inject constructor(name: String) { ... }
次要构造器
一个类也可以用 constructor 作为前缀来声明次要构造器(secondary constructor):
class Person {constructor(parent: Person) {parent.children.add(this);}}
如果一个类有首要构造器,那么每个次要构造器都要代理到首要构造器,可以直接或者通过其他次要构造器间接实现。使用 this 关键字可以把调用代理到同一个类的其他构造器:
class Person(val name: String) {constructor(name: String, parent: Person) : this(name) {parent.children.add(this)}}
注意:初始化区块里的代码会成为首要构造器的一部分。次要构造器的第一条语句会代理到首要构造器,因此初始化区块中的代码会优先执行。即使没有首要构造器,代理也会静默发生,所以初始化区块仍然会被执行到。
class Constructors {init {println("Init block")}constructor(i: Int) {println("Constructor")}}
如果一个非抽象的类没有声明任何构造器(首要或次要),那么它会自动生成一个 public 的无参首要构造器。如果不想要 public 构造器,可以声明一个非默认可见性并且空的首要构造器。
class DontCreateMe private constructor () {}
注意:在 JVM 上,如果首要构造器的所有参数都有默认值,编译器额外会生成一个使用默认值的无参构造器。这样可以方便 Jackson 或者 JPA 这样的库通过无参构造器来创建类的实例。
class Customer(val customerName: String = "")
创建类的实例
创建类实例时,可以像调用普通函数那样调用构造器:
val invoice = Invoice()val customer = Customer("Joe Smith")
注意,Kotlin 没有 new 关键字。
创建嵌套、内部和匿名类的实例在“嵌套类”那一章节。
类的成员
一个类可以包含:
- 构造器和初始化区块
- 函数
- 属性
- 嵌套和内部类
- 对象声明
继承
Kotlin 中所有的类都有一个共同的父类 - Any,对于没有声明父类,那么Any 就是默认父类。
class Example // Implicitly inherits from Any
注意:
Any不是java.lang.Object,特别的是,它除了equals()、hashCode()和toString()之外,没有任何其他成员。
如果要显示声明一个父类,需要把父类的类型放置在冒号之后:
open class Base(p: Int)class Derived(p: Int) : Base(p)
这里的
open注解与 Java 的final正好相反:它允许这个类被继承。默认情况下,Kotlin 中所有的类都是 final,正好符合 Effective Java 的第 17 条:要么为继承而设计,并提供文档说明,要么就禁止继承。
如果继承的类有首要构造器,那么它的基类可以(并且必须)使用首要构造器的参数在它出现的地方进行初始化。
如果这个类没有首要构造器,那么每一个次要构造器必须使用 super 关键字来初始化基类,或者调用另一个实现了这个要求的构造器。注意,在这种情况下,不同的次要构造器可以调用不同的基类构造器。
class MyView : View {constructor(ctx: Context) : super(ctx)constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)}
方法覆写
就像前面提到的,Kotlin 倾向于让事物变得明确。与 Java 不同的是,对于可覆写的成员(可被称之为 open)和覆写本身,Kotlin 需要用注解明确地标记:
open class Base {open fun v() {}fun nv() {}}class Derived() : Base() {override fun v() {}}
override 必须标记在 Derived.v() 上,否则会导致编译出错。如果函数没有 open 注解,像 Base.nv(),在子类中声明一个同样签名的方法是非法的,不管有没有用 override。final 类(没有 open 注解的类)不允许有 open 成员。
用 override 标注的成员自身是 open,也就是说,它可以在子类中被覆写。如果想禁止再被重载,可以使用 final:
open class AnotherDerived() : Base() {final override fun v() {}}
属性覆写
属性覆写的工作方式和方法覆写一样,在父类中声明的属性,如果想要在子类中重新声明,必须使用 override 标注,并且类型必须兼容。每个声明的属性都可以被覆写为自带初始化或者带有 getter 方法的属性。
open class Foo {open val x: Int get() { ... }}class Bar1 : Foo() {override val x: Int = ...}
也可以用 var 属性来覆写 val 属性,但是反过来不可以。原因是:val 属性本质上声明了一个 getter 方法,用 var 覆写就等价于在继承的类中额外定义了一个 setter 方法。
注意,override 可以用在首要构造器的属性声明中。
interface Foo {val count: Int}class Bar1(override val count: Int) : Fooclass Bar2 : Foo {override var count: Int = 0}
继承类的初始化顺序
在构造一个继承类的实例时,完成基类的初始化是第一步(只在基类构造器参数的评估之后),因此要早于继承类执行它的初始化逻辑。
open class Base(val name: String) {init { println("Initializing Base") }open val size: Int =name.length.also { println("Initializing size in Base: $it") }}class Derived(name: String,val lastName: String) : Base(name.capitalize().also { println("Argument for Base: $it") }) {init { println("Initializing Derived") }override val size: Int =(super.size + lstName.length).also { println("Initializing size in Derived: $it") }}
这就意味着,基类构造器在执行时,继承类所声明或覆写的属性还没有被初始化。如果这些属性被用到了基类的初始化逻辑中(无论是直接还是间接地通过另一个覆写的 open 成员),可能会导致不正确的行为或者运行时失败。所以设计基类时,在构造器、属性初始化以及 init 块中要避免使用 open 成员。
调用父类实现
使用 super 关键字,子类中的代码可以调用父类的函数和属性访问器。
open class Foo {open fun f() { println("Foo.f()") }open val x: Int get() = 1}class Bar : Foo() {override fun f() {super.f();println("Bar.f()")}override val x: Int get() = super.x + 1}
内部类如果要访问外部类的父类,访问方式是 super 再加上外部类类名:
class Bar : Foo() {override fun f() { /* ... */ }override val x: Int get() = 0inner class Baz {fun g() {super@Bar.f() // Calls Foo's implementation of f()println(super@Bar.x)}}}
覆写规则
在 Kotlin 中,实现继承要遵守如下规则:如果一个类间接地从多个父类中继承了同一个成员的多个实现,那么它必须覆写这个成员并且提供自己的实现(也可以直接使用继承中的某一个)。我们可以采用用 super 加上尖括号包裹父类名字的方式来标记继承实现是来自哪个父类,例如,super<Base>。
open class A {open fun f() { print("A") }fun a() { print("a") }}interface B {fun f() { print("B") } // interface members are 'open' by defaultfun b() { print("b") }}class C() : A(), B {// the compile requires f() to be overridden:override fun f() {super<A>.f() // call to A.f()super<B>.f() // call to B.f()}}
可以同时继承 A 和 B ,a() 和 b() 也没有问题,因为 C 只继承了这些方法当中的一个实现。但是 C 继承了两个 f() 的实现,因此我们必须要在 C 中重写 f() 来提供我们自己的实现,这样才能排除二义性。
抽象类
类和它的某些成员可以声明为 abstract。类的抽象成员没有实现。我们没必要用 open 来标注一个抽象的类或函数,因此这是显而易见(It goes without saying)。
我们把非抽象的 open 成员覆写为抽象的:
open class Base {open fun f() {}}abstract class Derived : Base() {override abstract fun f()}
联合对象
与 Java 或者 C# 不同的是,Kotlin 的类没有静态方法。大部分情况下,推荐使用包级别的函数来替代。
如果我们需要写一个函数,这个函数可以不通过类的实例来调用,但是它需要访问到类本身的内部(例如,一个工厂方法),那么在那个类中将其写成一个对象声明类型的成员。
更具体一点,如果在类中声明了一个联合对象,就可以使用像 Java/C# 静态方法那样的语法来调用它的成员——只使用类名作为限定符。
