kotlin入门潜修

kotlin入门潜修之类和对象篇—继承

2018-12-01  本文已影响0人  寒潇2018

本文收录于 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) {//子类,子类必须要实现父类中的一个构造方法
}

有几点需要注意:

  1. kotlin中的类默认是final的,即是无法继承的,这与java不同,java中默认都是可继承的。kotlin中所有的设计都是要显示提供,其实这也正是kotlin的设计理念,只有在真正需要的时候才暴露。kotlin提供了open关键字用于显示表明该类是可继承的。
  2. 子类必须要实现父类中的一个构造方法。可以通过子类的主构造方法去初始化父类构造方法,也可以通过第二构造方法初始化父类的构造方法。上面的例子就是通过主构造方法初始化了父类。第二构造方法初始化示例如下:
//父类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<String>) {
            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<A>.m1()//这里调用A类中m1的实现,非强制,可选择性调用
        super<B>.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<String>) {
            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<String>) {//main方法执行入口
        }
    }
}

当然,也可以提供包级别的main方法,如下所示:

class Main {
//作为对比,这里暂时注释掉了伴随对象
//    companion object {
//        @JvmStatic fun main(args: Array<String>) {
//
//        }
//
//    }
}
//这里提供了package-level的main入口方法,作用同上面注释掉的伴随对象写法。
fun main(args: Array<String>) {
}
上一篇下一篇

猜你喜欢

热点阅读