本文收录于 kotlin入门潜修专题系列,欢迎学习交流。
创作不易,如有转载,还请备注。
写在前面
用平常心来生活,用惭愧心来待人,用心来处事,用菩提心契佛心。——与君共勉。
继承
面向对象的三大基石:继承、多态与封装。这三个特性构成了绚丽多彩的编程世界,也衍生出了诸多优雅的设计。本篇文章将会解析kotlin中的继承机制。
众所周知,java中所有的类都会默认继承java.lang.Object类,同样,kotlin中所有的类也默认继承了一个叫做Any的类,其作用同java的Object类,是kotlin里面所有类的基类。
需要注意的是,Any类虽然同java中的Object类一样作为所有类的基类存在,但是Any类并不等同于java的Object类,因为Any类中只有equals、hasCode、toString三个方法,而java中的Object类还有诸如getClass、notifyAll、wait、clone等方法,所以二者并不是一个类。
kotlin中的继承写法也和java完全不一样了,kotlin中不再有extends、implements关键字,取而代之的是冒号“ : ”,其定义如下:
open class Person constructor(name: String) {//基类,注意有open关键字修饰
}
class Student(name: String) : Person(name) {//子类,子类必须要实现父类中的一个构造方法
}
有几点需要注意:
- kotlin中的类默认是final的,即是无法继承的,这与java不同,java中默认都是可继承的。kotlin中所有的设计都是要显示提供,其实这也正是kotlin的设计理念,只有在真正需要的时候才暴露。kotlin提供了open关键字用于显示表明该类是可继承的。
- 子类必须要实现父类中的一个构造方法。可以通过子类的主构造方法去初始化父类构造方法,也可以通过第二构造方法初始化父类的构造方法。上面的例子就是通过主构造方法初始化了父类。第二构造方法初始化示例如下:
//父类People,注意,这里提供了一个主构造方法和一个第二构造方法
open class People constructor(name: String) {
public constructor(name: String, age: Int) : this(name)
}
//下面是几种不同的初始化父类的写法
//1. 通过第二构造方法初始化,这里调用了父类People的主构造方法
class Teacher : People {
constructor() : super("张三")
}
//2. 通过第二构造方法初始化,这里调用了父类People的第二构造方法
class Teacher : People {
constructor() : super("张三", 10)
}
//3.通过主构造方法初始化,这里调用了父类People的主构造方法
class Teacher(name: String) : People (name){
}
//4.通过主构造方法初始化,这里调用了父类People的第二构造方法
class Teacher(name: String) : People (name, 20){
}
在实际编码中,具体采用上面哪种写法可以根据场景自行选择。主要能够保证初始化父类的任意构造方法即可。
复写方法(Overriding Methods)
kotlin中方法的复写和类的设计理念一样(类必须显示定义为open才能被继承),必须要显示指定该方法可以复写,子类才能进行复写(当然前提是父类也必须定义为可继承的,即要open修饰),其显示指定的关键字依然是open。示例如下:
//父类,open修饰,表示可继承
open class Person {
fun getAge(){}//注意这里没有open关键字
open fun getName(){}//这里有open关键字
}
class Student() : Person() {
override fun getName() {//这里override是合法的,因为父类该方法使用了open修饰,表示可以被复写
super.getName()
}
override fun getAge(){}//!!! 这是不合法的,编译不通过!因为父类中的getAge()并没有显示指定为open
fun getAge(){}//!!! 这也是不合法的,编译不通过!因为父类中已经存在getAge(),只能override。在这个例子中即使override也是不合法的,上面已经阐述。
}
一个方法一旦被标记为open方法,那么该方法就一直能被override(即其子类的子类的子类...等等都可以复写),那么如果子类不想再让其子类override方法怎么办?比如上个例子中,Person中的getName是可被override的,所以子类Student可以通过override fun getName来复写,但是现在Student不在期望其子类再override getName方法,该怎么办?很简单,在其方法前加final关键字即可:
open class Student() : Person() {
final override fun getName() {//注意这里加了final关键字,表示其子类不再能复写该方法。
super.getName()
}
}
复写属性(overriding properties)
复写属性和复写方法一样,要用open显示标明可复写。属性的继承有几点需要注意的,示例如下
//父类,该类设置为了可继承,即open修饰
open class Person {
var age : Int = 20
var height: Int = 170
open var address : String = "address"
val name : String = "name"
open val email : String = "email"
open val phoneNum : Int = 1234567
open var score: Int = 80
open val sex : String get() {return "男"}
}
//子类,继承Person,分析的重点就在这里。
class Student : Person() {
//首先看var变量
var age: Int = 20//!!!编译不通过,父类已经存在该字段。
override var height: Int = 180//!!!编译不通过,因为父类中没有显示定义为open,故不能复写。
override var address: String = "address"//正确,因为父类中显示定义为了open
//下面是val变量
val name: String = "name"//!!!编译不通过,父类已经存在该字段。
override val email: String = "email"//正确,因为父类中显示定义为了open
override var phoneNum : Int = 1234567//正确,注意,这里父类中的phoneNum是val不可变的,但这里复写为了var可变的,kotlin是允许这么做的。
override val score: Int = 80//!!!编译错误,注意,这里父类中的score是var可变的,而这里复写为了val不可变的,kotlin中是不允许这么做的。
override val sex: String get() {//正确,这里只是演示了属性变量另一种初始化方法,即使用get方法。
return "男"
}
}
上面基本分析了复写属性的各种情况,唯一需要注意的是父类中的val是可以在子类中被复写为var的,反之则不行。这是为什么?
是这样的,kotlin中的val属性都默认定义了一个getter方法,子类复写为var的时候实际上是为该变量额外增加了一个setter方法,所以可以这么做。
此外,kotlin也可以在主构造方法中复写属性,如下所示:
open class Person constructor(open val name: String) {
}
//注意,子类在主构造方法中复写了name属性
open class Student(override val name: String) : Person(name) {
}
派生类的初始化顺序
所谓派生类即是继承父类的子类。那么派生类的执行顺序是怎么样的?先看下面一个例子:
//父类
open class Person(name: String) {
init {
println("initializing person")
}
//这里运用了let方法,会在后续文章中分析
open val nameLength: Int = name.length.let {
println("initializing name length in person:".plus(it))
it
}
}
//子类
class Student(name: String, lastName: String) : Person(name.let { println("argument for person $it")
it }) {
init {
println("initializing student")
}//注意,这里看着比较绕,但是实际完成功能就是打印基类的入参
override val nameLength: Int = lastName.length.let {
(super.nameLength + it).let {
println("initializing name length in student:".plus(it))
it
}
}
}
//程序执行入口
@JvmStatic fun main(args: Array) {
var student = Student("name", "lastName")//生成student对象
}
上面代码执行main方法后,会打印一下日志:
argument for person name
initializing person
initializing name length in person:4
initializing student
initializing name length in student:12
通过日志打印可以看出,kotlin会首先初始化父类,父类先执行构造方法,然后按编码顺序先后执行init块、属性初始化等,接着会执行子类构造方法、init块、属性初始化等。
由此可知,在父类执行构造方法的时候,子类的属性或者复写父类的属性都还没有初始化,所以父类中一定不能使用这些属性,否则会造成未知的错误,甚至会造成运行时异常。
因此,在设计父类的时候,一定要避免在构造方法、属性初始化以及init块中使用open类型的成员变量(因为这些晚些时候可能会被子类复写)。
调用父类中的实现
kotlin同java一样,子类要调用父类的实现可以通过super关键字完成,示例如下:
//父类
open class Person() {
open fun printSex() {
println("默认性别:男")
}
var defaultName = ""
open val age = 20
}
//子类
class Student() : Person() {
override fun printSex() {//复写父类printSex方法
super.printSex()//这里通过super调用父类中方法
println("the student age: 18")
}
fun printName(){//子类自定义打印姓名的方法
println(super.defaultName)//这里直接调用了父类中的非open属性。
}
override val age: Int
get() = super.age + 2//这里通过super调用父类中的open属性
}
kotlin中,只要父类中的实现(属性或者方法)不是private的,子类都可以通过super来调用父类的实现。
复写规则
这里的复写规则讲的是,当一个子类实现多个父实现的时候,会存在多个父实现含有相同实现的情形(如含有相同的方法签名或者相同的属性)。注意,kotlin同java一样,依然是单继承体系,即一个子类一次只能继承一个父类,这里所说的父实现是指,子类可能会在继承父类的同时实现了一个或者多个接口。具体示例如下:
//父类A,有m1和m2两个方法
open class A {
open fun m1() {
print("m1 in A")
}
open fun m2() {
print("m2 in A")
}
}
//接口B,有m1和m3两个方法,注意m1方法和A中的签名一样。
interface B {//kotlin中接口的写法,使用关键字interface修饰
fun m1() {//接口中的方法默认都是open的,所以不需要使用open修饰
print("m1 in B")
}
fun m3() {
print("m3 in B")
}
}
//实现类C,继承了A同时实现了B接口
class C : A(), B {//多个实现的写法使用英文逗号(,)隔开
//注意这里,因为A类中有方法m1,B接口中也有方法m1,所以子类就不知道该默认实现哪个父实现中的方法。因此,在这种情形下,kotlin会强制子类明确复写该方法。如果子类还想调用父类的实现,那么可以通过super<父类型>这种方法来指定调用父类的实现,
override fun m1() {//该方法必须要复写
super.m1()//这里调用A类中m1的实现,非强制,可选择性调用
super.m1()//调用B接口中m1的实现
}
}
上面代码中,由于m1存在实现冲突(两个父实现都有该方法),所以子类必须要复写该方法,而m2、m3不存在冲突,故kotlin不强制复写。
抽象类
kotlin中的抽象类同java一样,都是使用abstract关键字来修饰。kotlin中的抽象类,默认都是open的,所以不需要再显示使用open关键字进行修饰。如果一个类的任意一个成员被定义为abstract,那么该类必须要定义为抽象类。
示例如下:
abstract class A {//抽象类使用abstract修饰
abstract fun m1()//抽象方法不能有任何实现,即不能有方法体{}
open fun m3() {//抽象类可以包含普通的方法实现
print("m3 in A")
}
}
//子类C,继承抽象类A
class C : A() {
//子类必须要实现抽象类中的抽象方法。普通方法则不强制实现。
override fun m1() {
}
}
伴随对象
伴随对象是kotlin中特有的存在。kotlin不像java、c#,它没有static方法,而是推荐使用包级别(package-level)的方法替代,示例如下:
package com.test//com.test包
fun staticM1(){//直接定义了一个staticM1方法,注意这里并没有定义任何类
println("staticM1")
}
//在Main类中调用该包级别方法
import com.test.staticM1//导入了staticM1方法
class Main {
companion object {//这个是个伴随对象,下面会分析
@JvmStatic fun main(args: Array) {
staticM1()//这里调用了staticM1,使用方法如同java中的static,没有生成任何类对象
}
}
}
上面的写法即是包级别的方法,大部分都可以满足要使用“静态方法”的需求。从代码也可以看出,包级别的方法不依附于任何类,也就是不属于任何类。但是假如有个方法需要在一个类中定义,而我们确实又需要在不生成该类实例的情况下使用该方法,该怎么办呢(如工厂方法模式)?
针对这种情况,kotlin提供了另一个实现机制:伴随对象。有了伴随对象,就可以想调用静态方法一样使用了,如下所示:
class A {
companion object {//伴随对象的写法,两个关键字companion object
fun m1() {//这里定义了一个m1方法,注意下面B类中的调用方式
println("method m1 in A's companion object")
}
}
}
class B {
fun test() {
A.m1()//注意这里,通过A类名调用了m1方法,而没有生成A类实例
}
}
实际上,我们前面已经多次用到伴随对象了,比如程序的执行入口Main类中main方法的实现。我们都知道java中的执行入口是静态方法,那么kotlin中的执行入口该怎么写呢?示例如下:
class Main {
companion object {//伴随对象
@JvmStatic fun main(args: Array) {//main方法执行入口
}
}
}
当然,也可以提供包级别的main方法,如下所示:
class Main {
//作为对比,这里暂时注释掉了伴随对象
// companion object {
// @JvmStatic fun main(args: Array) {
//
// }
//
// }
}
//这里提供了package-level的main入口方法,作用同上面注释掉的伴随对象写法。
fun main(args: Array) {
}