# Kotlin教程 - 8 面向对象
# 8.7 多态
# 1 什么是多态
多态就是多种状态,同一个类型的父类型对象,因为指向的是不同的子对象,而表现出的不同的状态。
所以多态是建立在继承的基础之上的。
举个栗子:
open class Animal {
open fun makeSound() {}
}
class Dog : Animal() {
override fun makeSound() {
println("我会汪汪叫")
}
}
class Cat : Animal() {
override fun makeSound() {
println("我会喵喵叫")
}
}
fun main() {
val animal1: Animal = Dog() // 创建一个狗对象
val animal2: Animal = Cat() // 创建一个猫对象
animal1.makeSound() // 我会汪汪叫
animal2.makeSound() // 我会喵喵叫
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
上面狗类和猫类都继承自动物类,然后创建了一个狗对象和猫对象,都赋给了动物类,通过两个对象分别调用 makeSound() 方法。
虽然两个都是动物类的变量,执行的都是 makeSound() 方法,但是因为是不同的子类对象,却得到不同的结果。
以父类做定义声明,以子类做实际的工作,用于获取同一个行为的不同状态,这就是多态。
那也没看出多态有什么用啊。
多态的作用:
- 提高代码的维护性
- 提高代码的扩展性
- 把不同的子类对象当做父类来看待,可以屏蔽不同子类对象之间的差异,写出通用的代码,以适应需求的不断变化。
# 2 多态的使用
说了那么多,有点虚,举个栗子:
实现一个功能:学生做交通工具去某个地方,交通工具可能是汽车、飞机。
先定一个汽车类:
传入一个目的地,就可以开车去了。
class Car {
fun run(destination: String) {
println("开车去->${destination}")
}
}
2
3
4
5
再定义一个飞机类:
class Plane {
fun fly(destination: String) {
println("坐飞机->${destination}")
}
}
2
3
4
5
然后定义一个学生类:
class Student {
fun goTo(vehicle: Any, destination: String) { // 传入交通工具和目的地
if (vehicle is Car) { // 判断交通工具的类型,然后调用交通工具的方法
vehicle.run(destination)
} else if (vehicle is Plane) {
vehicle.fly(destination)
}
}
}
2
3
4
5
6
7
8
9
学生类有一个 goTo() 方法,接收交通工具和目的地,然后在方法中判断交通工具的类型,然后调用交通工具的方法。
调用代码:
fun main() {
val car = Car()
val plane = Plane()
val stu = Student()
stu.goTo(car, "北京") // 开车去->北京
stu.goTo(plane, "新疆") // 坐飞机->新疆
}
2
3
4
5
6
7
8
上面的代码可以实现功能,但是不易于扩展,如果我们现在增加一个交通工具火车,则还需要修改 Student 类的 goTo() 方法,针对新的交通工具来处理,因为学生类和汽车、飞机类直接存在依赖关系,耦合性高。
这样是违反了设计原则中的开闭原则,对扩展是开放的,对修改是封闭的,也就是允许在不改变它代码的前提下变更它的行为。
所以上面的代码扩展性就比较差了,那么怎么来优化代码,降低代码的耦合性呢?
这就需要用到多态了。
首先定义一个父类 Vehicle(交通工具类),并定义一个 transport()
方法,都是交通工具,都是运输功能嘛。
然后让 Car 类和 Plane 类都继承这个父类,因为不同的子类运输方式不一样,所以子类需要重写父类的方法,实现自己的功能。
open class Vehicle {
open fun transport(destination: String) {}
}
class Car : Vehicle() {
override fun transport(destination: String) {
println("开车去->${destination}")
}
}
class Plane : Vehicle() {
override fun transport(destination: String) {
println("坐飞机->${destination}")
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
然后修改学生类:
class Student {
fun goTo(vehicle: Vehicle, destination: String) {
vehicle.transport(destination)
}
}
2
3
4
5
学生类只需要调用交通工具的运输功能就可以了。
调用的代码不变:
fun main() {
val car = Car()
val plane = Plane()
val stu = Student()
stu.goTo(car, "北京") // 开车去->北京
stu.goTo(plane, "新疆") // 坐飞机->新疆
}
2
3
4
5
6
7
8
上面的代码使用了多态,学生类与各个交通工具子类已经不直接产生关系,遵从了设置原则中的依赖倒置原则(程序依赖于抽象接口,不要依赖于具体实现)。
此时如果新增一个火车的交通工具,不用再修改学生类的代码,代码耦合性大大降低。
# 3 抽象类
什么是抽象类?
含有抽象方法的类成为抽象类。
那什么是抽象方法?
抽象方法就是没有方法体,方法体为空的方法。
上面的 Vehicle
类中的 transport
方法,没有方法体,我们可以将它定义为一个抽象方法,将 Vehicle
定义为抽象类 。
在 class
前面加上 abstract
关键字,就是定义了抽象类。
abstract class Vehicle {
abstract fun transport(destination: String)
}
2
3
抽象类的特点:
- 抽象类不能被实例化,存在的目的是为了被子类继承和实现其抽象属性和方法。
- 和 Java 不同,抽象类中除了有抽象方法和非抽象方法,还包括抽象属性和非抽象属性。
- 子类继承抽象类需要实现(重写)抽象类中所有的抽象属性和方法,如果没有全部实现所有的抽象属性和方法,那么子类也必须是抽象类。
- 虽然抽象类不能被实例化,但是抽象类可以包含构造函数,构造函数可以在子类中被调用。
抽象类有什么作用呢?
抽象类是不能被实例化的,也就是不能用来创建对象。一般抽象类都是作为父类使用的,父类用来确定有哪些方法,相当于用来确定设计的标准,用于对子类的约束。
子类用来实现父类的标准,负责具体的实现,配合多态使用,获得不同的工作状态。
// 定义父类抽象类
abstract class Shape {
// 抽象属性
abstract val area: Double
// 抽象方法
abstract fun calculateArea(): Double
// 具体方法
fun display() {
println("Shape's area is ${area}")
}
}
// 定义父类
class Circle(private val radius: Double) : Shape() {
override val area: Double // 重写属性
get() = Math.PI * radius * radius
override fun calculateArea(): Double { // 重写方法
return Math.PI * radius * radius
}
}
fun main() {
val shape = Circle(1.0)
shape.calculateArea()
shape.display()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 4 接口
# 接口的定义
如果一个抽象类中的方法都是抽象方法,我们可以将这个抽象类定义成接口。
定义接口使用 interface
关键字。
举个栗子:
// 定义一个接口
interface Helpful {
fun help() // 抽象方法
}
class Dog : Helpful { // 类实现接口
override fun help() {
println("我会看门")
}
}
fun main() {
var dog = Dog()
dog.help()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
一般说子类继承父类,子类实现接口,类实现接口的方式和继承父类基本是一样的。不同的是接口中是没有构造函数的,所以类实现接口的时候,接口名后面没有 ()
。
接口中的的方法,不需要添加 open 关键字,默认就是可以被重写的。
结合前面多态的使用去理解,前面多态的使用中,使用的是父类来完成的,我们也可以使用接口来完成。
代码:
interface Vehicle { // 定义交通工具的接口,所有的交通工具都需要实现该接口
fun transport(destination: String)
}
class Car : Vehicle { // 需要实现交通工具接口
override fun transport(destination: String) {
println("开车去->${destination}")
}
}
class Plane : Vehicle { // 需要实现交通工具接口
override fun transport(destination: String) {
println("坐飞机->${destination}")
}
}
class Student {
fun goTo(vehicle: Vehicle, destination: String) { // 传入的参数是交通工具,交通工具都必须有transport()方法
vehicle.transport(destination)
}
}
fun main() {
val car = Car()
val plane = Plane()
val stu = Student()
stu.goTo(car, "北京") // 开车去->北京
stu.goTo(plane, "新疆") // 坐飞机->新疆
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 接口中的属性
其实抽象类中也是可以定义属性的。但是抽象类中的属性是抽象的,也就是说抽象类中的属性是没有办法存储数值的,只是声明!
举个栗子:
// 定义一个接口
interface Helpful {
var level: Int
get() = 12 // 这里不能返回field,但是这里返回的12相当于level的默认值
set(value) {} // 这里不能设置field
open fun help()
}
class Dog(level: Int) : Helpful { // 类实现接口
override var level: Int = level // 重写接口中的属性
get() = field // 这里的getter和setter和默认的实现相同,所以可以不写
set(value) {field = value}
override fun help() {
println("我会看门")
}
}
fun main() {
var dog = Dog(5)
dog.help()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在上面的代码中,在接口中定义了属性,但是接口中的属性是抽象的,不能存储值,所以需要在子类中重写属性。
虽然接口中的属性没有办法存储数值,但是可以通过 getter 返回数值,例如上面的 level 返回了12,也就相当于属性的默认值了。
# 接口中的方法
接口中的方法可以不是抽象的,也是可以有默认实现的。
举个栗子:
// 定义一个接口
interface Helpful {
var level: Int
get() = 12 // 这里不能返回field
set(value) {} // 这里不能设置field
fun help() { // 非抽象方法
println("我是有益的")
}
}
class Dog(level: Int) : Helpful { // 类实现接口
override var level: Int = level
get() = field
set(value) {field = value}
}
fun main() {
var dog = Dog(5)
dog.help() // 我是有益的
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在上面的代码中,接口中的方法是有实现的,所以子类也可以直接使用。
# 实现多个接口
在讲继承的时候,我们说过 Kotlin 中是没有多继承的,但是接口是可以实现多个的。
举个栗子:
// 定义一个抽象类
abstract class Animal {
open fun makeSound() {
println("我会叫")
}
}
// 定义一个接口
interface Helpful {
fun help()
}
// 定义一个接口
interface Running {
fun run()
}
class Dog : Animal(), Helpful, Running {
override fun makeSound() {
println("我会汪汪叫")
}
override fun help() {
println("我会看门")
}
override fun run() {
println("我会跑")
}
}
fun main() {
val dog = Dog()
dog.makeSound() // 我会汪汪叫
dog.help() // 我会看门
dog.run() // 我会跑
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
上面我们定义了一个抽象类 Animal
,和两个接口 Helpful
、Running
。然后定义了一个 Dog
类作为子类继承 Animal
类,同时实现了 Helpful
、Running
两个接口。
一个类只能有一个父类,但是可以有多个接口。一个类需要实现抽象类和接口中所有的抽象方法,否则编译会报错。
如果多个接口中有同名的方法,那么子类调用的是哪个接口中的方法呢?
在 Kotlin 中,如果一个类同时实现了多个接口,并且这些接口中包含具有相同签名的默认方法,则会导致冲突,编译器无法确定应该使用哪个默认方法。这种情况下,需要在类中显式地重写冲突的方法。
举个栗子:
// 定义一个接口
interface Helpful {
fun show() {
println("Helpful show")
}
}
// 定义一个接口
interface Running {
fun show() {
println("Running show")
}
}
class Dog : Helpful, Running {
override fun show() {
println("Dog show")
super<Helpful>.show() // 也可以调用指定接口中的方法
super<Running>.show()
}
}
fun main() {
val dog = Dog()
dog.show()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
在上面的代码中,两个接口存在同名的方法,子类就必须重写该方法,子类还可以指定调用哪个接口的方法。
如果父类中和接口中存在同名的方法,也是会冲突的,也是要重写冲突的方法,和上面接口冲突是一样的。
# 8.8 类的扩展
# 1 扩展函数
扩展函数可以在不直接修改类定义的情况下,增加类功能。扩展可以用于自定义的类,也可以用于List、String等标准库中的类。
在无法接触某个类定义或者无法继承某个类的时候,扩展就是增加类功能最好的办法。
Java 中无法实现,JavaScript中有类似的功能。
举个栗子:
给 String 类增加一个函数,在字符串后面添加一个 .png
的后缀名。
// 向 String 类添加一个扩展函数
fun String.addPngExt(): String {
return this + ".png"
}
fun main() {
val hello = "hello"
println(hello.addPngExt()) // hello.png
}
2
3
4
5
6
7
8
9
通过 String.addPngExt()
的方式,为 String
类添加了一个 addPngExt()
方法。后面通过 String
类的对象可以调用该方法,为字符串添加一个后缀名。
注意:
- 扩展函数在使用时像普通函数一样调用,但实际上并未更改原始类的定义。
- 扩展函数不能访问类的私有或受保护成员。
- 扩展函数不能覆盖已存在的函数。
- 在同一个类上定义了同名的扩展函数,后面定义的会覆盖之前定义的。
通过扩展函数,我们可以为所有的类添加方法,例如我们可以为 Any 类添加扩展函数。
举个栗子:
// 向 Any 类添加一个扩展函数
fun Any.sayHello() {
println("Hello, ${this}")
}
fun main() {
val hello = "Doubi"
hello.sayHello() // Hello, Doubi
}
2
3
4
5
6
7
8
9
如果在扩展函数上添加 private 修饰符,那么只会在当前文件中生效,默认是 public,整个项目生效。
# 2 泛型扩展函数
在上面我们通过给 Any
类添加了扩展函数,这样所有的类都有这个函数了。
但是这样有一个局限性,举个栗子:
// 向 Any 类添加一个扩展函数
fun Any.sayHello(): Any {
println("Hello, ${this}")
return this
}
// 向 String 类添加一个扩展函数
fun String.addPngExt(): String {
return this + ".png"
}
fun main() {
val hello = "Doubi"
//hello.sayHello().addPngExt(); // 报错,无法调用addPngExt()函数
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在上面的代码中,字符串调用完 sayHello()
函数后,无法再链式调用 addPngExt()
函数了,因为 sayHello()
函数返回的是 Any
对象。
针对这个问题,我们可以使用泛型扩展函数。将上面的 sayHello()
函数定义为泛型函数。
举个栗子:
// 定义扩展函数
fun <T> T.sayHello(): T {
println("Hello, ${this}")
return this
}
// 向 String 类添加一个扩展函数
fun String.addPngExt(): String {
return this + ".png"
}
fun main() {
val hello = "Doubi"
val result = hello.sayHello().addPngExt()
println(result) // Doubi.png
15.sayHello() // Hello, 15,这样都可以调用
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在上面定了泛型扩展函数,这样所有类型的对象和数据都可以调用泛型扩展函数。
之前讲到的扩展函数 let
、apply
都是通过泛型扩展函数实现的。
# 3 扩展属性
除了给类添加功能扩展函数外,还可以给类定义扩展属性。
例如:我们可以给 String 类添加一个扩展属性,用于统计字符串中有多少个字符是数字。
举个栗子:
// String 添加扩展属性
val String.numCount
get() = count { "0123456789".contains(it) }
fun main() {
var str = "abc1234"
println(str.numCount) // 4
}
2
3
4
5
6
7
8
在上面的代码中,给 String
类添加了一个 numCount
扩展属性,通过 getter
计算出字符串中数字有多少个。
# 4 可空类型扩展函数
对可空类型进行函数扩展,可以是某些操作更加方便。
先举个不使用扩展函数的例子:
fun main() {
var str: String? = "abc"
println(str.length) // 报错:可空类型无法直接使用length属性获取长度
}
2
3
4
5
上面的代码报错,可空类型 String?
无法直接使用 length
属性获取长度。
我们可以对可空类型进行扩展。
举个栗子:
// 对可空类型进行扩展
fun String?.safeLength(): Int {
return this?.length ?: 0 // 如果字符串为空,返回 0;否则返回字符串的长度
}
fun main() {
var str: String? = null
println(str.safeLength()) // 0
str = "abc"
println(str.safeLength()) // 3
val str2:String = "abcd";
println(str2.safeLength()) // 4,非空类型也可以调用
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在上面的代码中,首先对可空类型 String?
进行了扩展,如果字符串为空,返回长度0,否则返回字符串长度。
注意,这里是直接调用的,没有?.safeLength()。
这样无论是空、非空,甚至是非空类型的字符串都可以调用该方法,非常的方便。
# 5 infix关键字
在前面讲解 Map 的时候,讲过 to 其实是一个方法。
举个栗子:
fun main() {
val pair = "doubi" to "abc"
println(pair.first)
}
2
3
4
通过 to 函数可以创建 Pair 对象。
这是什么函数,为什么可以这样调用。
其实是通过 infix
关键字实现的。
infix 关键字适用于有单个参数的扩展和类函数,可以使用更简洁的语法调用函数。如果定义函数的时候使用 infix
关键字,那么在调用的时候,调用者和参数之间的点以及参数的括号都可以省略。
举个栗子:
// 对String?类型进行扩展
infix fun String?.printWithDefault(default: String) {
println(this ?: default) // 如果为空就打印defalut
}
fun main() {
val str: String? = null
str printWithDefault "hello" // 就可以奇奇怪怪的写了
}
2
3
4
5
6
7
8
9
在上面的代码中使用 infix
修饰了函数,函数只有一个参数,调用函数就可以省略点和括号了。
理解不了为什么发明这种东西。
# 6 定义扩展文件
当扩展函数需要在多个文件中使用的时候,我们可以将扩展函数定义在单独的文件中,然后导入。
例如创建一个 StringExtension.kt
文件,并编辑内容如下:
package com.doubi.extension
// 对 String? 类型进行扩展
fun String?.safeLength(): Int {
return this?.length ?: 0 // 如果字符串为空,返回 0;否则返回字符串的长度
}
// 向 String 类添加一个扩展函数
fun String?.addPngExt(): String {
return this + ".png"
}
2
3
4
5
6
7
8
9
10
11
这样在其他文件中就可以引入并使用了:
Main.kt
import com.doubi.extension.safeLength // 引入
import com.doubi.extension.addPngExt
fun main() {
val str1: String? = null
println(str1.safeLength()) // 0
val str2 = "abc"
println(str2.addPngExt()) // abc.png
}
2
3
4
5
6
7
8
9
10
首先使用 import 引入扩展函数,然后就可以使用了。
当然引入的时候,也可以使用通配符 com.doubi.extension.*
。
在引入的时候,甚至还可以起别名,举个栗子:
import com.doubi.extension.safeLength as slength
fun main() {
val str: String? = null
println(str.slength()) // 0
}
2
3
4
5
6
上面的代码中,在引入的时候,通过 as
关键字,为引入的扩展函数起了别名,这样就可以使用别名来调用了。
# 7 apply
查看 apply
函数的实现,发现主要的代码就是通过泛型扩展函数实现的。下面讲解一下 apply
函数的实现
主要代码如下:
public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}
2
3
4
如果我们不使用泛型来定义扩展函数,单纯给 File 对象来定义扩展函数的话,可以这样写:
public inline fun File.apply(block: File.() -> Unit): File {
block()
return this
}
2
3
4
这样就可以看到,apply
函数是一个扩展函数, 同时 apply
函数的参数其实也是一个扩展函数。
继续分解一下:
import java.io.File
// 定义 apply 扩展函数
inline fun File.apply(block: File.() -> Unit): File {
block()
return this
}
// 给File定义了一个扩展函数
fun File.block() {
setReadable(true) // 扩展函数中可以直接调用this的方法
setWritable(true)
}
fun main() {
var file = File("E:\\text.txt")
file.apply(File::block) // 直接将定义的扩展函数传给了apply函数
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在上面的代码中,首先定义了 apply
函数,apply
函数的参数也是一个扩展函数。所以我们又定义了一个扩展函数 block
,然后将 block
函数传递给了 apply
函数。最主要的是使用了扩展函数中this的隐式调用来实现了链式调用!
所以回过头来再看 apply
函数的参数 block: T.() -> Unit
就可以看出来,这个函数的类型是一个泛型的扩展函数了。