《Kotlin从小白到大牛》第12章:继承与多态

第12章 继承与多态

类的继承性是面向对象语言的基本特性,多态性的前提是继承性。Kotlin支持继承性和多态性。本章讨论Kotlin继承性和多态性。

12.1 Kotlin中的继承

为了了解继承性,先看这样一个场景:一位面向对象的程序员小赵,在编程过程中需要描述和处理个人信息,于是定义了类Person,如下所示:
//代码文件:chapter12/src/com/a51work6/section1/Person.kt
package com.a51work6.section1

import java.util.*

class Person {
// 名字
val name: String? = null
// 年龄
val age: Int = 0
// 出生日期
val birthDate: Date? = null

val info: String
    get() = ("Person [name=$name, age=$age, birthDate=$birthDate]")

}
一周以后,小赵又遇到了新的需求,需要描述和处理学生信息,于是他又定义了一个新的类Student,如下所示:
//代码文件:chapter12/src/com/a51work6/section1/Student.kt
package com.a51work6.section1

import java.util.*

class Student {
// 所在学校
val school: String? = null
// 名字
val name: String? = null
// 年龄
val age: Int = 0
// 出生日期
val birthDate: Date? = null

val info: String   
    get() = ("Person [name=$name, age=$age, birthDate=$birthDate]")

}

很多人会认为小赵的做法能够理解并相信这是可行的,但问题在于Student和Person两个类的结构太接近了,后者只比前者多了一个属性school,却要重复定义其他所有的内容,实在让人“不甘心”。Kotlin提供了解决类似问题的机制,那就是类的继承,代码如下所示:
//代码文件:chapter12/src/com/a51work6/section1/Student.kt
package com.a51work6.section1

import java.util.*

class Student : Person() { ①
// 所在学校
val school:String? = null

override val info: String
    get() =("Person [name=$name,age=$age,birthDate=$birthDate]")

}

//代码文件:chapter12/src/com/a51work6/section1/Person.kt
package com.a51work6.section1

import java.util.*

open class Person {//: Any() { ②
// 名字
val name:String? = null
// 年龄
val age: Int =0
// 出生日期
val birthDate:Date? = null

open val info:String
    get() =("Person [name=$name,age=$age,birthDate=$birthDate]")

}
上述代码可见Student类继承了Person类中的成员属性和函数,代码第①行声明Student继承Person,继承使用的冒号(:),冒号前是子类,冒号后是父类。
如果在类的声明中没有使用指明其父类,则默认父类为Any类,kotlin.Any类是Kotlin的根类,所有Kotlin类包括数组都直接或间接继承了Any类,在Any类中定义了一些有关面向对象机制的基本函数,如equals、toString和hashCode等函数。
子类能够继承父类,那么父类需要声明为open,见代码第②行,在Kotlin中默认类不能被继承必须声明为open的。
《Kotlin从小白到大牛》第12章:继承与多态_第1张图片

12.2 调用父类构造函数

当子类实例化时,不仅需要初始化子类成员属性,也需要初始化父类成员属性,初始化父类成员属性需要调用父类构造函数。
修改12.1节示例,父类Person代码如下:
//代码文件:chapter12/src/com/a51work6/section2/Person.kt
package com.a51work6.section2

import java.util.*

open class Person(val name: String,
val age: Int,
val birthDate: Date)
{ //主构造函数
//次构造函数
constructor(name: String, age: Int) :this(name, age, Date())

override fun toString(): String {
    return("Person [name=$name, age=$age, birthDate=$birthDate]")
}

}
Person类中有两个构造函数,分别是一个主构造函数和一个次构造函数。子类Student继承Person类有多重实现方式,下面分别介绍一下。

12.2.1 使用主构造函数
在子类Student中可以声明主构造函数和次构造函数。示例代码如下:
//代码文件:chapter12/src/com/a51work6/section2/s1/Student.kt
package com.a51work6.section2.s1

import com.a51work6.section2.Person
import java.util.*

class Student(name: String,
age: Int,
birthDate: Date,
val school: String) :Person(name, age, birthDate) { //主构造函数 ①

constructor(name: String, //次构造函数                 
            age: Int,
            school: String) :this(name, age, Date(), school)    //  super(name, age, Date())     ②
            
constructor(name: String, //次构造函数
            school: String) :this(name, 18, school)      //super(name, 18, Date())                ③

}
上述代码第①行是声明Student的主构造函数,主构造函数中val school: String参数,说明会生成属性school,Person(name,
age, birthDate)表达式是调用父类构造函数。代码第②行是声明Student的次构造函数,this(name, age, Date(), school)是调用自己的主构造函数帮助完成初始化,如果将this(name, age, Date(), school)表达式换成super(name, age, Date())则会发生编译错误,super(name, age, Date())是次构造函数中调用父构造函数。代码第③行也是声明Student的次构造函数,this(name, 18, school)是调用自己的代码第②行的次构造函数帮助完成初始化,如果将this(name, 18, school)表达式换成super(name, 18, Date())则会发生编译错误,super(name, 18, Date())是次构造函数中调用父构造函数。
在这里插入图片描述

12.2.2 使用次构造函数重载
在子类Student中可以不声明主构造函数,可以声明多个次构造函数。示例代码如下:
//代码文件:chapter12/src/com/a51work6/section2/s2/Student.kt
package com.a51work6.section2.s2

import com.a51work6.section2.Person
import java.util.*

class Student : Person {

// 所在学校
private var school: String? = null

constructor(name: String,
            age: Int,
            birthDate: Date,
            school: String) :super(name, age, birthDate) {              ①
    this.school = school
}

constructor(name: String,
            age: Int,
            school: String) : this(name,age, Date(), school) {           ②
    this.school = school
}

}
上述代码第①行和第②行都是声明次构造函数,其中代码第①行的次构造函数中super(name, age, birthDate)表达式是调用父构造函数。代码第②行的次构造函数中this(name, age, Date(), school)表达式是调用代码第①行自己的次构造函数。
在这里插入图片描述
12.2.3 使用参数默认值调用构造函数
一个类有多个多个造函数时,多个构造函数之间构成了重载关系,Kotlin从语法角度是支持重载的,但更推荐采用参数默认值方式。
示例代码如下:
//代码文件:chapter12/src/com/a51work6/section2/s3/Student.kt
package com.a51work6.section2.s3

import com.a51work6.section2.Person
import java.util.*

class Student : Person {

// 所在学校
private var school: String? = null

constructor(name: String,
            age: Int = 18, 
            birthDate: Date = Date(),
            school: String) :super(name, age, birthDate) {
    this.school = school
}

}

上述代码中只是声明了一个次构造函数,它有4个参数,其中age和birthDate参数提供了默认值。这样声明相当于提供了3个构造函数,调用代码如下:
//代码文件:chapter12/src/com/a51work6/section2/s3/ch12.2.1.kt
package com.a51work6.section2.s3

import java.util.*

fun main(args: Array) {
val stu1 = Student(“Tony”,20, Date(), “清华大学”)
val stu2 = Student(“Tony”,birthDate = Date(9823456), school = “清华大学”)
val stu3 = Student(“Tony”,school = “清华大学”)
}

上述代码只有一个次构造函数,事实上也可以只有一个主构造函数,示例代码如下:
//代码文件:chapter12/src/com/a51work6/section2/s3/Student.kt
package com.a51work6.section2.s3
class Student(name: String,
age:Int = 18,
birthDate: Date = Date(),
val school: String) : Person(name, age, birthDate) {

val info:String
    get() =("Student [name=$name,age=$age,birthDate=$birthDate,school=$school]")

}

12.3 重写成员属性和函数

子类继承父类后,在子类中有可能声明了与父类一样的成员属性或函数,那么会出现什么情况呢?

12.3.1 重写成员属性
子类成员属性与父类一样,会重写(Override)父类中的成员属性,也就是屏蔽了父类成员属性。示例代码如下:
//代码文件:chapter12/src/com/a51work6/section3/s1/ch12.3.1.kt
package com.a51work6.section3.s1

open class ParentClass {
// x成员属性
open var x = 10 ①
}

internal class SubClass : ParentClass() {
// 屏蔽父类x成员属性
override var x = 20 ②

fun print() {
    // 访问子类x成员属性
    println("x = " + x)                       ③
    // 访问父类x成员属性
    println("super.x = " +super.x)      ④
}

}

调用代码如下:
//代码文件:chapter12/src/com/a51work6/section3/s1/ch12.3.1.kt
package com.a51work6.section3.s1

fun main(args: Array) {

//实例化子类SubClass
val pObj =SubClass()

//调用子类print函数
pObj.print()
}

运行结果如下:
x = 20
super.x = 10
上述代码第①行是在ParentClass类声明x成员属性,那么在它的子类SubClass代码第②行也声明了x成员属性,它会屏蔽父类中的x成员属性。那么代码第③行的x是子类中的x成员属性。如果要调用父类中的x成员属性,则需要super关键字,见代码第④行的super.x。
在这里插入图片描述
12.3.2 重写成员函数
如果子类函数完全与父类函数相同,即:相同的函数名、相同的参数列表和相同的返回类型,只是函数体不同,这称为子类重写(Override)父类函数。
示例代码如下:
//代码文件:chapter12/src/com/a51work6/section3/s3/ch12.3.2.kt
package com.a51work6.section3.s2

open class ParentClass {
// x成员属性
open var x: Int = 0 ①

open protected fun setValue() { ②
    x = 10                              ③
}

}

class SubClass : ParentClass() {
// 屏蔽父类x成员属性
override var x: Int = 0 ④

public override fun setValue() { // 重写父类函数    ⑤
    // 访问子类对象x成员属性
    x = 20                                       ⑥
    // 调用父类setValue()函数
    super.setValue()                 ⑦
}

fun display() {
    // 访问子类对象x成员属性
    println("x = " + x)
    // 访问父类x成员属性
    println("super.x = " +super.x)
}

}

调用代码如下:
//代码文件:chapter12/src/com/a51work6/section3/s3/ch12.3.2.kt
package com.a51work6.section3.s2

fun main(args: Array) {

//实例化子类SubClass
val pObj = SubClass()
//调用setValue函数
pObj.setValue()
//调用子类print函数
pObj.display()

}
上述代码第②行是在ParentClass类声明setValue函数,那么在它的子类SubClass代码第⑤行重写父类中的setValue函数,在声明函数时添加override关键字声明。当在main函数中调用子类setValue函数时,首先在代码第⑥行修改x属性为20。紧接着在代码第⑦行调用父类的setValue函数,在该函数中将x属性修改为10,注意此时修改的属性是子类中x属性(代码第④行声明的属性),而不是父类中的x属性(代码第①行声明的属性),所以最后输出的结果是。
x = 10
super.x = 0
在这里插入图片描述
《Kotlin从小白到大牛》第12章:继承与多态_第2张图片

12.4 多态

在面向对象程序设计中多态是一个非常重要的特性,理解多态有利于进行面向对象的分析与设计。

12.4.1 多态概念
发生多态要有三个前提条件:
1.继承。多态发生一定要子类和父类之间。
2.重写。子类重写了父类的函数。
3.声明对象类型是父类类型,对象是子类的实例。
下面通过一个示例理解什么多态。如图12-1所示,父类Figure(几何图形)类有一个onDraw(绘图)函数,Figure(几何图形)它有两个子类Ellipse(椭圆形)和Triangle(三角形),Ellipse和Triangle重写onDraw函数。Ellipse和Triangle都有onDraw函数,但具体实现的方式不同。
《Kotlin从小白到大牛》第12章:继承与多态_第3张图片
具体代码如下:
//代码文件:chapter12/src/com/a51work6/section4/s1/Ellipse.kt
package com.a51work6.section4.s1

open class Figure {

//绘制几何图形函数
open fun onDraw() {
    println("绘制Figure...")
}

}

//代码文件:chapter12/src/com/a51work6/section4/s1/Ellipse.kt
package com.a51work6.section4.s1

//几何图形椭圆形
class Ellipse : Figure() {
//绘制几何图形函数
override fun onDraw() {
println(“绘制椭圆形…”)
}
}

//代码文件:chapter12/src/com/a51work6/section4/s1/Ellipse.kt
package com.a51work6.section4.s1

//几何图形三角形
class Triangle : Figure() {
// 绘制几何图形函数
override fun onDraw() {
println(“绘制三角形…”)
}
}

调用代码如下:
//代码文件:chapter12/src/com/a51work6/section4/s1/ch12.4.1.kt
package com.a51work6.section4.s1

fun main(args: Array) {

// f1变量是父类类型,指向父类实例
val f1 = Figure()         ①
f1.onDraw()

// f2变量是父类类型,指向子类实例,发生多态
val f2: Figure = Triangle()          ②   

f2.onDraw()

// f3变量是父类类型,指向子类实例,发生多态
val f3: Figure = Ellipse()             ③
f3.onDraw()

// f4变量是子类类型,指向子类实例
val f4 = Triangle()      ④
f4.onDraw()

}
上述带代码第②行和第③行是符合多态的三个前提,因此会发生多态。而代码第①行和第④行都不符合,没有发生多态。
运行结果如下:
绘制Figure…
绘制三角形…
绘制椭圆形…
绘制三角形…
从运行结果可知,多态发生时,Kotlin运行时根据引用变量指向的实例调用它的函数,而不是根据引用变量的类型调用。

12.4.2 使用is和!is进行类型检查
有时候需要在运行时判断一个对象是否属于某个类型,这时可以使用is或!is运算符,语法格式如下:
obj is type // obj对象是type类型实例,则返回true
obj !is type //
obj对象不是type类型实例,则返回true
其中obj是一个对象,type是数据类型。
为了介绍引用类型检查,先看一个示例,如图12-2所示的类图,展示了继承层次树,Person类是根类,Student是Person的直接子类,Worker是Person的直接子类。
《Kotlin从小白到大牛》第12章:继承与多态_第4张图片
继承层次树中具体实现代码如下:
//代码文件:chapter12/src/com/a51work6/section4/s2/Person.kt
package com.a51work6.section4.s2

open class Person(val name: String, val age: Int) {

override fun toString(): String {
    return ("Person [name=$name,age=$age]")
}

}

//代码文件:chapter12/src/com/a51work6/section4/s2/Student.kt
package com.a51work6.section4.s2

class Student(name: String, age: Int, private val school:
String) : Person(name, age) {
override fun toString(): String {
return(“Student [school= s c h o o l , n a m e = school,name= school,name=name,age=$age]”)
}
}
//代码文件:chapter12/src/com/a51work6/section4/s2/Worker.kt
package com.a51work6.section4.s2

class Worker(name: String, age: Int, private val factory:String) : Person(name, age) {

override fun toString(): String {
    return ("Worker [factory=$factory,name=$name,age=$age]")
}

}

调用代码如下:
//代码文件:chapter12/src/com/a51work6/section4/s2/ch12.4.2.kt
package com.a51work6.section4.s2

fun main(args: Array) {

val student1 =Student("Tom", 18, "清华大学")      ①
val student2 =Student("Ben", 28, "北京大学")
val student3 =Student("Tony", 38, "香港大学")      ②

val worker1 =Worker("Tom", 18, "钢厂")      ③
val worker2 =Worker("Ben", 20, "电厂")       ④

val people =arrayOf(student1, student2, student3, worker1, worker2) ⑤

varstudent Count = 0
var worker Count= 0

for (item in people) {        ⑥
    if (item is Worker) {   ⑦           

workerCount++
} else if (item is Student) { ⑧
studentCount++
}
}
println(“工人人数: w o r k e r C o u n t , 学 生 人 数 : workerCount,学生人数: workerCountstudentCount”)
println(worker2 !is Worker) ⑨
println(0 is
Int) ⑩
}

输出结果如下:
工人人数:2,学生人数:3
false
true

上述代码第①行和第②行创建了3个Student实例,代码第③行和第④行创建了两个Worker实例,然后程序把这5个实例放入people数组中。
代码第⑥行使用for循环people数组集合,当从people数组中取出元素时,元素类型是People类型,但是实例不知道是哪个子类(Student和Worker)实例。代码第⑦行item is Worker表达式是判断数组中的元素是否是Worker实例;类似地,第⑧行item is Student表达式是判断数组中的元素是否是Student实例。
代码第⑨行是使用!is判断worker2不是否Worker实例,结果为false。
代码第⑩行是使用is基本数据类型0是否为Int类型实例,可见is和!is也可以用于基本数据类型。

12.4.3 使用as和as?进行类型转换
在6.3节介绍过数值类型相互转换,引用类型也可以进行转换,但并不是所有的引用类型都能互相转换,只有属于同一棵继承层次树中的引用类型才可以转换。
在上一节示例上修改代码如下:
//代码文件:chapter12/src/com/a51work6/section4/s3/ch12.4.3.kt
package com.a51work6.section4.s3

fun main(args: Array) {

val p1: Person =Student("Tom", 18, "清华大学")
val p2: Person =Worker("Tom", 18, "钢厂")

val p3 = Person("Tom", 28)
val p4 = Student("Ben", 40,"清华大学")
val p5 = Worker("Tony", 28,"钢厂")


}
上述代码创建了5个实例p1、p2、p3、p4和p5,它们的类型都是Person继承层次树中的引用类型,p1和p4是Student实例,p2和p5是Worker实例,p3是Person实例。首先,对象类型转换一定发生在继承的前提下,p1和p2都声明为Person类型,而实例是由Person的子实例化的。
表12-2归纳了p1、p2、p3、p4和p5这5个实例与Worker、Student和Person这3种类型之间的转换关系。
《Kotlin从小白到大牛》第12章:继承与多态_第5张图片
引用类型转换有两个方向:将父类引用类型变量转换为子类类型,这种转换称为向下转型(downcast);将子类引用类型变量转换为父类类型,这种转换称为向上转型(upcast)。向下转型需要使用as或as?运算符进行强制转换;而向上转型是自动的,也可以使用as运算符。
在这里插入图片描述
下面通过示例详细说明一下向下转型和向上转型,在main函数中添加如下代码:
// 向上转型
val p41: Person = p4 //as Person ①
val p51 = p5 as Person ②

// 向下转型
val p11= p1 as Student ③
val p21= p2 as Worker ④

val p211 = p2 as? Student //使用as会发生运行时异常 ⑤
val p111 = p1 as? Worker //使用as会发生运行时异常 ⑥
val p311 = p3 as? Student //使用as会发生运行时异常 ⑦
上述代码第①行将p4对象转换为Person类型,p4本质上是Student实例,这是向上转型,这种转换是自动的,可以使用as进行强制类型转换。代码第②行没有声明p51类型,而是将p5对象转换为Person类型,这个过程可以成功。
代码第③行和第④行是向下类型转换,它们的转型都能成功。而代码第⑤、⑥、⑦行转换类型是不兼容的,如果as进行转换会发生运行时异常ClassCastException,所以这里使用了as?进行转换,当然转换的结果都是空值。

12.5 密封类

如果一个类它的子类的个数是有限的,那么在Kotlin中可以把这种父类定义为密封类(Sealed Classes),密封类是一种抽象类,它限定了子类个数。密封类类似于枚举类,
枚举类中每个常量实例只能有一个,而密封类的子类实例可以有多个。
下面通过示例介绍一下密封类使用,在进行数据库操作时,会出现成功和失败两种情况。如果采用密封类设计,代码如下:
//代码文件:chapter12/src/com/a51work6/section5/ch12.5.kt
package com.a51work6.section5

sealed class Result ①
class Success(val message: String) : Result() ②
class Failure(val error: Error) : Result() ③

fun onResult(result: Result) {
when (result) { ④
is Success-> println(" r e s u l t 输 出 成 功 消 息 : {result}输出成功消息: result{result.message}")
is Failure-> println(" r e s u l t 输 出 失 败 消 息 : {result}输出失败消息: result{result.error.message}")
//else-> 不再需要
}
}

fun main(args: Array) {

val result1 =Success("数据更新成功")   

onResult(result1)
val result2 =Failure(Error(“主键重复,插入数据失败”))
onResult(result2)
}
上述代码第①行是声明一个密封类Result,使用sealed修饰。密封类本身就是抽象的不需要abstract修饰,一定也是open的,密封类不能实例化。代码第②行和第③行是都是声明密封类的子类,但是Succuess和Failure内部结构是不同的,Succuess有一个字符串属性message,而Failure有一个Error类型属性。
代码第④行使用when结果判定密封类实例,注意不再需要else结构。
在这里插入图片描述
密封类的子类还可以写成嵌套类形式,这是Kotlin1.1之前密封类规范,Kotlin1.1在仍然可以使用这些形式。示例代码如下:
sealed class ContentType { ①
class Text(val body: String) : ContentType() ②
class Image(val url: String, val caption: String) : ContentType() ③
class Audio(val url: String, val duration: Int) : ContentType() ④
}

fun renderCotent(contentType: ContentType): Unit {
when (contentType)
{ ⑤
isContentType.Text -> println(“文本: c o n t e n t T y p e . b o d y " ) ⑥ i s C o n t e n t T y p e . A u d i o − > p r i n t l n ( " 音 频 : {contentType.body}") ⑥ isContentType.Audio -> println("音频: contentType.body")isContentType.Audio>println(":{contentType.duration}秒”) ⑦
isContentType.Image -> println(“图片:${contentType.caption}”) ⑧
}
}
上述代码第①行是声明密封类ContentType,它表示浏览器能够渲染的内容。ContentType内部嵌套三个子类,代码第②行Text是文本子类,代码第③行Image是图片子类,代码第④行Audio是音频子类,这三个子类都有不同的结构和不同的构造函数。
代码第⑤行使用when结构判断ContentType子类实例,代码第⑥行~第⑧行是判断为文本子类实例,注意访问子类时需要添加前缀ContentType。

本章小结

通过对本章的学习,首先介绍了Kotlin中的继承概念,在继承时会发生函数的重写、属性的隐藏。然后介绍了Kotlin中的多态概念,广大读者需要熟悉多态发生的条件,掌握引用类型检查和类型转换。最后介绍了密封类。

你可能感兴趣的:(Kotlin从小白到大牛)