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

本文收录于 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>) {
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,018评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,046评论 2 372
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,215评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,303评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,181评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,171评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,577评论 3 384
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,260评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,546评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,630评论 2 311
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,390评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,260评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,633评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,920评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,210评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,569评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,767评论 2 335

推荐阅读更多精彩内容

  • Kotlin的类和接口与Java的类和接口是有一定的区别的。Kotlin的接口是可以包含属性声明。Kotlin默认...
    程自舟阅读 10,301评论 0 11
  • 前言 人生苦多,快来 Kotlin ,快速学习Kotlin! 什么是Kotlin? Kotlin 是种静态类型编程...
    任半生嚣狂阅读 26,119评论 9 118
  • 写在开头:本人打算开始写一个Kotlin系列的教程,一是使自己记忆和理解的更加深刻,二是可以分享给同样想学习Kot...
    胡奚冰阅读 1,403评论 5 11
  • 面向对象编程(OOP) 在前面的章节中,我们学习了Kotlin的语言基础知识、类型系统、集合类以及泛型相关的知识。...
    Tenderness4阅读 4,407评论 1 6
  • 对于回顾这段历史。 用战争,屠杀,血腥,只会引来更多的仇恨。这是经历过这段历史的所有人都想避免的。 美丽人生,最悲...
    JabinW阅读 190评论 0 0