Kotlin 笔记 3.1:类和继承

类和继承

Kotlin 和 Java 一样同样使用 class 关键字来声明一个类,但是不同的是,如果类没有类体的话可以将大括号省略。

抽象类

类以及其中的某些成员可以声明为 abstract。 抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用 open 标注一个抽象类或者函数——因为这不言而喻。

我们可以用一个抽象成员覆盖一个非抽象的开放成员

open class Base {
    open fun f() {}
}
​
abstract class Derived : Base() {
    override abstract fun f()
}

构造方法

Kotlin 中的构造方法和 Java 有所不同,在 Kotlin 中,构造方法是由主构造方法和次构造方法组成的。

主构造函数

主构造方法有些特殊,它并不是像 Java 一样写在大括号内,而是直接跟在类名后面:

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

那个 constructor 关键字就是表明这是一个构造方法,它后面的是主构造方法的参数,如果主构造方法没有任何注解或者可见性修饰符,那么这个 constructor 关键字可以省略:

//省略 constructor 关键字
class Person(var name: String, var age: Int) {
    ... 
}
//下面就不能省略,因为加了可见性修饰符 - private 
class Person private constructor(var name: String, var age: Int) {
    ...
}

还记得 Java 中声明构造方法不?

class Person{
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

在 Java 语言中,通过 this.xxx = xxx 来给属性赋值,在 Kotlin 语言中,这样是不允许的(主构造方法跟类写到一块儿去了,咋这样写),初始化语句需要放在初始化代码块中,初始代码块用 init 关键字标识,:

class Person constructor(name: String, age: Int) {
    var name: String
    var age: Int

    init {
        this.name = name
        this.age = age
    }
}

使用初始化代码块之外,还可以直接在类中进行属性的初始化:

class Person constructor(name: String, age: Int) {
    var name: String = name
    var age: Int = age
}

事实上,还有更简便的方法:

class Person constructor(var name: String, var age: Int) {
}

上面这三种方法的效果是一样的,也不知道 Kotlin 为啥这么设计,咱也不懂,咱也不敢问,也能这就是所谓的“语法糖”吧。

次构造函数

次构造函数是声明在类内部的,同样是使用 constructor 关键字,但是这个 constructor 关键字,就不能省略了。
如果这个类有主函数,那么每个次函数都需要使用关键字 this 直接或者通过其他次构造函数间接委托给主函数:

class Person constructor(var name: String, var age: Int) {
    constructor(name: String) : this(name, 20) {

    }

    constructor(age: Int) : this("张三", age) {

    }
}

如果一个类没有声明任何主/次构造函数,那么系统会默认生成一个不带参数的主构造函数(跟 Java 一样),这个默认的主构造函数是 public 的。

创建类实例

Kotlin 中没有 new 关键字,所有创建类的实例的时,直接使用构造函数就可以:

var person = Person("张三",20)

继承

和 Java 中的 Object 一样,Kotlin 中的类也有一个超类:Any,如果一个类没有声明父类,那么其默认的父类就是 Any,但是这个 Any 又和 Object 稍有不同,在 Kotlin 中,Any 类只有 equals()hashCode()toString() 方法,而在 Object 中,有 wait() notify()getClass() 等方法。

我们使用关键字 : 来指定一个继承关系,就像 Java 中的 extends 一样,父类放在冒号后面。

如果我们想要自己的可以被继承,需要将该类显式标识为 open,如果父类有主构造函数,那么其子类也必须使用主构造函数将父类的参数初始化,如果父类没有主构造函数,那么在子类当中的所有次构造方法都必须显式使用 super 关键字为父类的参数初始化:

open class Person {
    var name: String

    constructor(name: String) {
        this.name = name
    }

}

class Student : Person {
    constructor(name: String, age: Int) : super(name) {

    }
}

重写方法

如果我们父类的方法可以被子类重写,那么该方法必须显式声明为 open,子类使用关键字 override 进行标识,如果不再希望方法被重写,可以再重新将之设置为 final 类型:

open class Person {
    var name: String

    constructor(name: String) {
        this.name = name
    }

    open fun say(word: String) {
        println("父类:$word")
    }

}

class Student : Person {
    constructor(name: String, age: Int) : super(name) {

    }

    final override fun say(word: String) {
        super.say(word)
        println("子类:$word")
    }
}

重写属性

重写属性的方法和重写方法一样,同样也是父类的属性标识为 open,子类使用 override 关键字,但是需要注意的是,我们可以用一个 var 类型去重写一个 val 属性,反之则不行,因为 var 属性本质包含了 setter 方法和 getter 方法,而 val 只包含 getter 方法,使用 var 覆盖 val,只不过额外声明了一个 setter 方法罢了。

重写规则

有时候,我们会同时继承了父类,也实现了接口,会遇到接口和父类方法名重名的时候,那么我们该如何表示自己重写的究竟是接口的方法还是父类的方法呢?Kotlin 提供了这样的方法:

open class Person {
    open fun say(word: String) {
        println("Person 父类:$word")
    }
}

interface People {
    fun say(word: String) {

    }
}


class Student : People, Person() {
    override fun say(word: String) {
        super<Person>.say(word) //调用父类的方法
        super<People>.say(word) //调用接口的方法
    }
}

调用父类方法

和 Java 一样,子类也可以通过 super 关键字去调用其父类的方法,在一个内部类中访问外部类的超类,可以通过由外部类名限定的 super 关键字来实现:super@Outer

class Bar : Foo() {
    override fun f() { /* …… */ }
    override val x: Int get() = 0
    
    inner class Baz {
        fun g() {
            super@Bar.f() // 调用 Foo 实现的 f()
            println(super@Bar.x) // 使用 Foo 实现的 x 的 getter
        }
    }
}