封装、继承和多态是面向对象编程的三大特性。
- 封装,把对象的属性和函数结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。
- 继承,从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和函数,并根据实际需求扩展出新的行为。
- 多态,多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的函数。
正是这三大特性,能够让我们的Kotlin程序更加生动形象。
类的封装
封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员,如果不进行封装,类中的实例变量可以直接查看和修改,可能给整个程序带来不好的影响,因此在编写类时一般将成员变量私有化,外部类需要使用Getter和Setter函数来查看和设置变量。从这里开始,我们前面学习的权限访问控制就开始起作用了。
我们可以将之前的类进行改进:
class Student(private var name: String, private var age: Int) {
fun getName(): String = name
fun getAge(): Int = age
}
现在,外部需要获取一个学生对象的属性时,只能使用特定的函数进行获取,而不像之前一样可以随意访问对象的属性:
fun main() {
var student = Student("", 1)
//student.name 这样就不行了
println(student.getName())
}
这样的好处显而易见,其他地方只能拿到在内部某个成员属性引用的对象,而没办法像之前那样直接修改Student对象中某个成员属性。
同样的,如果要运行外部对对象中的属性进行修改,那么我们也可以提供对应的set函数:
class Student(private var name: String, private var age: Int) {
...
fun setName(name: String){ //使用set函数来修改
this.name = name
}
}
等等,这不就是我们之前讲的属性的getter和setter函数吗,没错,哪怕我们不手动编写,成员属性也会存在默认的。但是,除了直接赋值之外我们也可以设置更多参数才能给学生改名字:
class Student(private var name: String, private var age: Int) {
fun setName(name: String, upper: Boolean){
//判断是否upper来决定最终赋值的名字大写还是小写
this.name = if (upper) name.uppercase() else name.lowercase()
}
}
我们自己封装好的名字设置方法暴露给外部使用,而不让外部直接操作名字。
我们甚至还可以将主构造函数改成私有的,需要通过其他的构造函数来构造:
class Student private constructor(private var name: String, private var age: Int) {
constructor() : this("", 18)
}
封装思想其实就是把实现细节给隐藏了,外部只需知道这个函数是什么作用,如何去用,而无需关心实现,要用什么由类自己提供好,而不需要外面来操作类内部的东西去完成(你让我做一件事情,我自己的事情自己做,不要你来帮我安排)封装就是通过访问权限控制来实现的。
类的继承
前面我们介绍了类的封装,我们接着来看一个非常重要特性:继承。
在定义不同类的时候存在一些相同属性,为了方便使用可以将这些共同属性抽象成一个父类,在定义其他子类时可以继承自该父类,减少代码的重复定义,根据前面的访问权限等级,子类可以使用父类中所有非私有的成员。
比如说我们一开始使用的学生,那么实际上学生根据专业划分,所掌握的技能也会不同,比如体育生会运动,美术生会画画,土木生会搬砖,计算机生会因为互联网寒冬找不到工作,因此,我们可以将学生这个大类根据不同的专业进一步地细分出来:
虽然我们划分出来这么多的类,但是其本质上还是学生,也就是说学生具有的属性,这些划分出来的类同样具有,但是,这些划分出来的类同时也会拥有他们自己独特的技能。就好比大学里的学生无论什么专业都会打游戏,都会睡觉,逃课,考试抄答案,四六级过不了,只不过他们专业不同,学的的方向不一样,也就掌握了其他专业不具备的技能。
在Kotlin中,我们可以使用继承操作来实现这样的结构,默认情况下,Kotlin类是“终态”的(不能被任何类继承)要使类可继承,请用 open
关键字标记需要被继承的类:
open class Student { //被继承的类我们称为父类
val xxx = "学生证"
fun hello() = println("我会打招呼")
}
我们可以像下面这样来创建一个继承学生的类:
class ArtStudent : Student() { //以调用构造函数的形式进行声明
//这个类就是Student类的子类
fun draw() = println("我会画画") //子类中也可以继续编写自己独有的函数
}
类的继承可以不断向下,但是同时只能继承一个类,在Kotlin中不支持多继承,只不过套娃还是可以的:
open class Student
open class ArtStudent: Student() //继承了一级,相当于Student的儿子
open class SuperArtStudent: ArtStudent() //继承了两级,相当于Student的孙子
class SuperBigArtStudent: SuperArtStudent() //继承了三级,相当于Student的祖孙
当一个类继承另一个类时,属性会被继承,可以直接访问父类中定义的属性,除非父类中将属性的访问权限修改为 private
,那么子类将无法访问(但是依然是继承了这个属性的)比如下面的例子:
fun main() {
var student = ArtStudent()
student.hello() //虽然这里是ArtStudent对象,但是由于其继承的是Student,因此包含Student中的属性
student.draw() //自己的属性也可以使用
print(student.xxx) //不止函数,父类中的成员字段也是没有问题的
}
是不是感觉非常人性化,子类继承了父类的全部能力,同时还可以扩展自己的独特能力,就像一句话说的: 龙生龙凤生凤,老鼠儿子会打洞。这里需要特别注意一下,因为子类相当于是父类的扩展,但是依然保留父类的特性,所以说,在对象创建并初始化的时候,不仅会对子类进行初始化,也会优先对父类进行初始化:
open class Student() {
init { println("父类初始化") }
fun hello() = println("我会打招呼")
}
class ArtStudent() : Student() {
init { println("子类初始化") }
fun draw() = println("我会画画")
}
实际上这里就是在构造这个子类对象之前,调用了一次父类的构造函数,而我们用于继承指定的构造函数,就是会被调用的那一个。
因此,如果父类存在一个有参构造函数,子类同样必须在构造函数中调用:
open class Student(name: String, age: Int) {
fun hello() = println("我会打招呼")
}
//子类必须适配其父类的构造函数,因为需要先对父类进行初始化
//其实就是去调用一次父类的构造函数,填入需要的参数即可,这里的参数可以是当前子类构造方法的形参,也可以是直接填写的一个参数
class ArtStudent(name: String, age: Int) : Student(name, 18) {
fun draw() = println("我会画画")
}
如果父类存在多个构造函数,可以任选一个:
open class Student() {
constructor(str: String) : this()
constructor(str: String, age: Int) : this()
fun hello() = println("我会打招呼")
}
class ArtStudent : Student("小明", 18) { //任选一个父类构造函数即可
fun draw() = println("我会画画")
}
当子类只存在辅助构造函数时,需要使用super关键字来匹配父类的构造函数:
open class Student {
constructor(str: String)
constructor(str: String, age: Int)
fun hello() = println("我会打招呼")
}
//子类不写主构造函数时,可以直接在冒号后面添加父类类名
class ArtStudent : Student {
constructor(str: String) : super(str) //使用super来调用父类构造函数,super表示父类(超类)
constructor(str: String, age: Int) : super(str, age)
fun draw() = println("我会画画")
}
也可以去匹配子类中其他构造函数:
class ArtStudent : Student {
constructor(str: String) : this(str, 18) //也可以调用子类其他构造函数,但是其他构造函数依然要间接或直接调用父类构造函数
constructor(str: String, age: Int) : super(str, age)
fun draw() = println("我会画画")
}
如果子类既有主构造函数,也有辅助构造函数,那么其他辅助构造函数只能直接或间接调用主构造函数:
open class Student() {
constructor(str: String) : this()
constructor(str: String, age: Int) : this()
fun hello() = println("我会打招呼")
}
class ArtStudent() : Student() {
constructor(str: String) : this() //正确,必须直接或间接调用主构造函数
constructor(str: String, age: Int) : super(str, age) //报错,不能绕过主构造函数去匹配父类构造函数
fun draw() = println("我会画画")
}
是不是感觉玩法太多,都眼花缭乱了?实际上只要各位小伙伴心里面清楚下面的规则,就很好理解上面这一堆写法了:
- 构造函数相当于是这个类初始化的最基本函数,在构造对象时一定要调用
- 主构造函数因为可能存在一些类的属性,所以说必须在初始化时调用,不能让这些属性初始化时没有初始值
- 子类因为是父类的延展,因此,子类在初始化时,必须先初始化父类,就好比每个学生都有学生证,这是属于父类的属性,如果子类在初始化时可以不去初始化父类,那岂不是美术生可以没有学生证?显然是不对的。
优先级关系:父类初始化 > 子类主构造 > 子类辅助构造
属性的覆盖
有些时候,我们可以希望子类继承父类的某些属性,但是我们可能希望去修改这些属性的默认实现。比如,美术生虽然也是学生,也会打招呼,但是可能他们打招呼的方式跟普通的学生不太一样,我们能否对打招呼这个函数的默认实现进行修改呢?
我们可以使用 override
关键字来表示对于一个属性的重写(覆盖)就像这样:
open class Student {
//注意,跟类一样,函数必须添加open关键字才能被子类覆盖
open fun hello() = println("我会打招呼")
}
class ArtStudent : Student() {
fun draw() = println("我会画画")
//在子类中编写一个同名函数,并添加override关键字,我们就可以在子类中进行覆盖了,然后编写自己的实现
override fun hello() = println("哦哈哟")
}
覆盖之后,当我们使用子类进行打招呼时,函数会按照我们覆盖的内容执行,而不是原本的:
同样的,类的某个变量也是可以进行覆盖的:
open class Student {
open val test: String = "测试"
fun hello() = println("我会打招呼: $test") //这里拿到的test就会变成被覆盖掉的
}
class ArtStudent : Student() {
//对父类的变量进行覆盖,类型必须一样
override val test: String = "干嘛"
fun draw() = println("我会画画")
}
是不是感觉很神奇?不过对于可变的变量,似乎下面这样来的更方便?
open class Student {
var test: String = "测试"
fun hello() = println("我会打招呼: $test")
}
class ArtStudent : Student() {
init { test = "干嘛" }
fun draw() = println("我会画画")
}
有些时候为了方便,比如在父类中的属性,我们可以直接在子类的主构造函数中直接覆盖:
open class Student {
open val name: String = "大明"
fun hello() = println("我会打招呼,我叫: $name")
}
//在主构造函数中覆盖,也是可以的,这样会将构造时传入的值进行覆盖
class ArtStudent(override val name: String) : Student() {
fun draw() = println("我会画画")
}
fun main() {
val student = ArtStudent("小红")
student.hello()
}
虽然现在已经很方便了,但是现在又来了一个新的需求,打招呼不仅要有子类的特色,同时也要保留父类原有的实现,这个时候该怎么办呢?我们可以使用 super
关键字来完成:
open class Student {
open fun hello() = println("我会打招呼")
}
class ArtStudent : Student() {
fun draw() = println("我会画画")
override fun hello() { //覆盖父类函数
super.hello() //使用super.xxx来调用父类的函数实现,这里super同样表示父类
println("哦哈哟") //再写自己的逻辑
}
}
这样,我们在覆盖原本的函数时,也可以执行原本的实现,在一些对函数内容进行增强的常见,这种用法非常常见:
不过,由于存在我们之前讲解的的初始化顺序,下面的这种情况需要特别注意:
open class Student {
open val name: String = "小明"
init { println("我的名字是: ${name.length}") } //这里拿到的name实际上是还未初始化的子类name
}
class ArtStudent : Student() {
override val name = "大明"
}
fun main() {
val student = ArtStudent()
}
由于父类初始化在子类之前,此时子类还未开始初始化,其覆盖的属性此时没有初始值,根据不同平台的实现,可能会出现一些问题,比如JVM平台下,没有初始化的对象引用默认为 null
,那么这里就会直接报空指针异常:
很神奇对吧,这里的 name
属性明明是一个非可空的String类型,居然还会出现 null
的情况报空指针,因此,对于这些使用了 open
关键字的属性(函数、变量等)只要是在初始化函数、构造函数中使用,IDEA都会给出警告:
我们接着来讲一个很绕的东西,在使用一些子类的时候,我们实际上可以将其当做其父类来进行使用:
fun main() {
val student: Student = ArtStudent() //使用Student类型的变量接收一个ArtStudent类型的对象引用
}
之所以支持这样去使用,是因为子类本身就是对父类的延伸,因此将其当做父类使用,也是没有问题的。就好比我们无论是美术生还是体育生,都可以当做学生来用,都可以送去厂里实习打螺丝,不然不给毕业证。
只不过,如果我们将一个对象作为其父类使用,那么在使用时也只能使用其父类的一些属性,就相当于我们在使用一个父类的对象:
即使我们很清楚这里引用的对象是一个美术生,但是只能当做普通学生来用,这在后面的集合类中会经常用到,因为集合类往往存在多种不同的实现,但是我们只需要关心怎么用就行了,并且为了方便更换实现,所以一般使用集合类对应的接口来作为变量的类型。
那么,如果子类重写了父类的某个函数,此时我们以父类的形式去使用,结果会怎么样?
open class Student {
open fun hello() = println("大家好")
}
class ArtStudent : Student() {
override fun hello() = println("我姓🐴我叫🐴牛逼")
}
可以看到,虽然当做父类来使用,但是其本质是不会变的,所以说,这里执行的结果依然是子类的覆盖实现。
那么,如果项目中很多这种明明是子类但是拿来当做父类用,我们怎么去判断使用的对象到底是什么类型的呢?我们可以使用 is
关键字来进行类型判断,以下面的三个类为例:
open class Student
class ArtStudent : Student()
class SportStudent : Student()
现在我们进行类型判断:
fun main() {
val student: Student = ArtStudent()
println(student is ArtStudent) //true,因为确实是这个类型
println(student is SportStudent) //false,因为不是这个类型
println(student is Student) //true,因为是这个类型的子类
}
可以看到,使用is关键字可以精准地对类型进行判断,只要判断的对象是这个类或是这个类的子类,那么就会返回true作为结果。
如果我们明确某个变量引用的对象是什么类型,可以使用 as
关键字来进行强制类型转换:
fun main() {
val student: Student = ArtStudent()
if(student is ArtStudent) {
val artStudent = student as ArtStudent;
artStudent.draw() //强制类型转换之后,可以直接变回原本的类型去使用
}
}
不过,编译器非常智能,它可以根据当前的语境判断的类型自动进行类型转换:
val student: Student = ArtStudent()
if(student is ArtStudent) {
student.draw()
}
此时IDEA中会出现提示:
不仅仅是if判断的场景、包括when、while,以及 &&
||
等运算符都支持智能转换,只要上下文语境符合就能做到:
fun main() {
val student: Student? = ArtStudent()
//很明显这里是当student为ArtStudent时,根据语境直接智能转换
while (student is ArtStudent) student.draw()
//很明显如果这前面已经判断为真了,那肯定是这个类型,后面也可以智能转换
if(student is ArtStudent && student.draw())
}
不仅仅是这种场景,比如我们前面讲解的可空类型,同样支持这样的智能转换:
fun main() {
val student: Student? = ArtStudent()
student?.hello()
if (student != null) //判断到如果不为null
student.hello() //根据语境student智能转换为了非空Student类型
}
在处理一些可空类型时,为了防止出现异常,我们可以使用更加安全的 as?
运算符:
fun main() {
val student: Student? = ArtStudent()
val artStudent: ArtStudent? = student as? ArtStudent //当student为null时,不会得到异常,而是返回null作为结果
}
有了这些操作,类和对象在我们使用的过程中就逐渐开始千变万化了,后面我们还会继续认识更多的多态特性。
顶层Any类
在我们不继承任何类的情况下,实际上Kotlin会有一个默认的父类,所有的类默认情况下都是继承自Any类的。
这个类的定义如下:
/**
* Kotlin类继承结构中的根类. 所有Kotlin中的类都会直接或间接将Any作为父类
*/
public open class Any {
/**
* 判断某个对象是否"等于"当前对象,这里同样是对运算符"=="的重载,而具体判断两个对象相等的操作需要由子类来定义
* 在一些特定情况下,子类在重写此函数时应该保证以下要求:
* * 可反身: 对于任意非空值 `x`, 表达式 `x.equals(x)` 应该返回true
* * 可交换: 对于任意非空值 `x` 和 `y`, `x.equals(y)` 当且仅当 `y.equals(x)` 返回true时返回true
* * 可传递: 对于任意非空值 `x`, `y`, 和 `z`, 如果 `x.equals(y)` 和 `y.equals(z)` 都返回true, 那么 `x.equals(z)` 也应该返回真
* * 一致性: 对于任意非空值 `x` 和 `y`, 在多次调用 `x.equals(y)` 函数时,只要不修改在对象的“equals”比较中使用的信息,那么应当始终返回同样的结果
* * 永不等于空: 对于任意非空值 `x`, `x.equals(null)` 应该始终返回false
*/
public open operator fun equals(other: Any?): Boolean
/**
* 返回当前对象的哈希值,它具有以下约束:
*
* * 对同一对象多次调用该函数时,只要不修改对象上的equals比较中使用的信息,那么此函数就必须始终返回相同的整数
* * 如果两个对象通过`equals`函数判断为true,那么这两个对象的哈希值也应该相同
*/
public open fun hashCode(): Int
/**
* 将此对象转换为一个字符串,具体转换为什么样子的字符串由子类自己决定
*/
public open fun toString(): String
}
由于默认情况下类都是继承自Any,因此Any中定义的函数也是被继承到子类中了。
首先我们来看这个 equals
函数,它实际上是对 ==
这个运算符的重载,我们之前在使用一些基本类型的时候,就经常使用 ==
来判断这些类型是否相同,比如Int类型的数据:
fun main() {
val a = 10
val b = 20
println(a == b)
println(a.equals(b)) //跟上面的写法完全一样
}
经过前面的学习,我们知道这些基本类型本质上也是定义的类,实际上它们也是通过重写这个函数来实现这些比较操作的(一些基本类型会根据不同的平台进行编译优化,没法看源码)
我们可以看到,这个函数接受的参数类型是一个 Any?
类型:
public open operator fun equals(other: Any?): Boolean //我们上节课说到一个子类也可以被当做父类类型的变量去使用,所以说equals判断接受的参数为了满足不同的类型变量之间进行比较,直接使用顶层Any作为参数(考虑到会用到可空类型,所以说直接用了Any?作为参数类型)
到目前为止,我们认识了Kotlin中两种相等的判断方式:
- 结果上 相等 (
==
等价于equals()
) - 引用上 相等 (
===
判断两个变量是否都是引用的同一个对象)
我们在使用 equals
比较两个可空对象是否相等时,就像这样:
a == b
实际上会被翻译为:
a?.equals(b) ?: (b === null) //a如果为null那就直接判断b是不是也为null,否则直接调用a的equals函数并让b作为参数
当然可能会有小伙伴疑问,那不等于判断呢?实际上是一样的:
fun main() {
val a = "10"
val b = "20"
println(a != b)
println(!a.equals(b)) //等价于上面的写法
}
我们也可以为我们自己编写的类型重写 equals
函数,比如我们希望Student类型当名字和年龄相等时,就可以使用 ==
来判断为true,我们可以像这样编写:
class Student(val name: String, val age: Int) {
override fun equals(other: Any?): Boolean {
if(this === other) return true //如果引用的是同一个对象,肯定是true不多逼逼
if(other !is Student) return false //如果要判断的对象根本不是Student类型的,那也不用继续了
if(name != other.name) return false //判断名字是否相同
if(age != other.age) return false //判断年龄是否相同
return true //都没问题,那就是相等了
}
}
此时我们已经将其比较操作重写,我们可以来试试看:
fun main() {
val a = Student("小明", 18)
val b = Student("小红", 17)
val c = Student("小明", 18)
println(a == a) //返回true因为就是自己
println(a == b) //返回false因为名字和年龄不一样
println(a == c) //返回true因为名字和年龄完全一样
}
默认情况下,如果我们不重写类的 equals
函数,那么会直接对等号两边的变量进行引用判断 ===
判断是否为同一个对象。只不过,可以很清楚地看到IDEA提示我们:
实际上在我们重写类的 equals
函数时,根据约定,必须重写对于的hashCode函数,至于为什么,我们会在后续的集合类部分中进行介绍,这里我们暂时先不对hashCode函数进行讲解。
接着我们来看下一个,toString
函数用于快速将对象转换为字符串,只不过默认情况下,会像这样:
fun main() {
val a = Student("小明", 18)
println(a.toString())
println(a) //println默认情况下会直接调用对象的toString并打印,所以跟上面是一样的
}
可以看到打印的结果是对象的 类型@十六进制哈希值
的形式,在某些情况下,可能我们更希望的是转换对象的一些成员属性,这样我们可以更直观的看到对象的属性具有什么值:
class Student(val name: String, val age: Int) {
override fun toString(): String { //直接重写toString函数
return "Student(name='$name', age=$age)"
}
}
现在得到的结果,就是我们自定义的结果了:
抽象类
有些情况下,我们设计的类可能仅仅是作为给其他类继承使用的类,而其本身并不需要创建任何实例对象,比如:
open class Student protected constructor() { //无法构造这个父类,要求使用子类
open fun hello() = println("Hello World!")
}
class ArtStudent: Student() {
override fun hello() = println("原神") //两个子类都对hello进行了实现,采用各自的方式
}
class SportStudent: Student() {
override fun hello() = println("启动")
}
可以看到,在上面这个例子中,Student类的 hello
函数在子类中都会被重写,所以说除非在子类中调用父类的默认实现,否则一般情况下,父类中定义的函数永远都不会被调用。
就像我们说一个学生会怎么考试一样,实际上学生怎么考试是一个抽象的概念,但是由于学生的种类繁多,美术生怎么考试和体育生怎么考试,才是具体的一个实现。所以说,我们可以将学生类进行进一步的抽象,让某些函数或字段完全由子类来实现,父类中不需要提供实现。我们可以使用 abstract
关键字来将一个类声明为抽象类:
//使用abstract表示这个是一个抽象类
abstract class Student {
abstract val type: String //抽象类中可以存在抽象成员属性
abstract fun hello() //抽象类中可以存在抽象函数
//注意抽象的属性不能为private,不然子类就没法重写了
}
当一个子类继承自抽象类时,必须要重写抽象类中定义的抽象属性和抽象函数:
class ArtStudent: Student() {
override val type: String = "美术生"
override fun hello() = println("原神,启动!")
}
这是强制要求的,如果不进行重写将无法通过编译。同时,抽象类是不允许直接构造对象的,只能使用其子类:
当然,抽象类不仅可以具有抽象的属性,同时也具有普通类的性质,同样可以定义非抽象的属性或函数:
abstract class Student {
abstract val type: String
abstract fun hello()
fun test() = println("不会有人玩到大三了才开始学Java吧") //定义非抽像属性或函数,在子类中不强制要求重写
}
同时,抽象类也可以继承自其他的类(可以是抽象类也可以是普通类)
open class Test //直接继承一个普通的类
abstract class Student: Test(){
...
}
虽然抽象类可以继承一个普通的类,但是这依然不改变它是抽象类的本质,子类依然要按照上面的要求进行编写。
接口
由于Kotlin中不存在多继承的操作,我们可以使用接口来替代。
前面我们认识了抽象类,它可以具有一些定义而不实现的内容,而接口比抽象类还要抽象,一般情况下,他只代表某个确切的功能!也就是只能包含函数或属性的定义,所有的内容只能是 abstract
的,它不像类那样完整。接口一般只代表某些功能的抽象,接口包含了一系列内容的定义,类可以实现这个接口,表示类支持接口代表的功能。
比如,学生具有以下功能:
- 打游戏
- 睡懒觉
- 逃课
- 考试作弊
我们可以将这一系列功能拆分成一个个的接口,然后让学生实现这些接口,来表示学生支持这些功能。
在Kotlin中,要声明接口,我们可以使用 interface
关键字:
interface A {
val x: String //接口中所有属性默认都是abstract的(可省略关键字)
fun sleep() //接口中所有函数默认都是abstract的(可省略关键字)
}
interface B {
fun game()
}
class Student: A, B { //接口的实现与类的继承一样,直接写到后面,多个接口用逗号隔开
override val x: String = "测试" //跟抽象类一样,接口中的内容是必须要实现的
override fun sleep() = println("管他什么早八不早八的,睡舒服再说")
override fun game() = println("读大学就该玩游戏玩到爽")
}
可以看到,接口相比于抽象类来说,更加的纯粹,它不像类那样可以具有什么确切的属性,一切内容都是抽象的,只能由子类来实现。
只不过,在接口中声明的属性可以是抽象的,也可以为Getter提供默认实现。在接口中声明的属性无法使用 field
后背字段,因此在接口中声明的Setter无法使用 field
进行赋值:
interface A {
val x: String
get() = "666" //只能重写getter,不能直接赋值,因为默认情况下getter是返回的field的值,但是接口里不让用
}
interface A {
var x: String
get() = "666"
set(value) { /* 默认的setter会直接报错,因为使用了field字段 */ }
}
为了应对变化多端的需求,接口也可以为函数编写默认实现:
interface A {
//接口中的函数可以具有默认实现,默认情况下是open的,除非private掉
fun sleep() = println("管他什么早八不早八的,睡舒服再说")
}
这样一看,这函数可以写默认的实现那接口似乎变得不那么抽象了?这用着感觉好像跟抽象类没啥区别啊?接口跟类的最大区别其实就是状态的保存,这从上面的成员属性我们就可以看的很清楚。
接口也可以继承自其他接口,直接获得其他接口中的定义:
interface A{
fun sleep() = println("管他什么早八不早八的,睡舒服再说")
}
interface B{
fun game() = println("读大学就该玩游戏玩到爽")
}
interface C: A, B //接口的继承写法是一样的,并且接口继承接口是支持多继承的
class Student: C //直接获得ABC三个接口的功能
是不是感觉接口的玩法非常有意思?只不过玩的过程中,可能也会遇到一些麻烦,比如下面的这种情况:
interface A{
fun sleep() = println("管他什么早八不早八的,睡舒服再说")
}
interface B{
fun sleep() = println("7点起床学Java了,不能再睡了")
}
class Student: A, B //由于A和B都具有sleep函数,那现在到底继承谁的呢?
这种情况下,我们需要手动解决冲突,比如我们希望Student类采用接口B的默认实现:
class Student: A, B {
override fun sleep() { //手动重写sleep函数,自行决定如何处理冲突
super<B>.sleep() //使用super关键字然后添加尖括号指定对应接口,并手动调用接口对应函数
}
}
对于接口,我们可以像之前一样,将变量的类型设定为一个接口的类型,当做某一个接口的实现来使用,同时也支持 is
、as
等关键字进行类型判断和转换:
fun main() {
val a: A = Student()
a.sleep() //直接当做A接口用(只能使用A接口中定义的内容)
println(a is B) //判断a引用的对象是否为B接口的实现类
}
是不是感觉跟之前使用起来是差不多的?其实只要前面玩熟悉了,后面还是很简单的。
类的扩展
Kotlin提供了扩展类或接口的操作,而无需通过类继承或使用装饰器等设计模式,来为某个类添加一些额外的函数或是属性,我们只需要通过一个被称为扩展的特殊声明来完成。
例如,您可以从无法修改的第三方库中为类或接口编写新函数,这些函数可以像类中其他函数那样进行调用,就像它们是类中的函数一样,这种机制被称为扩展函数。还有扩展属性,允许您为现有类定义新属性。
比如我们想为String类型添加一个自定义的操作:
//为官方的String类添加一个新的test函数,使其返回自定义内容
fun String.test() = "666"
fun main() {
val text = "Hello World"
println(text.test()) //就好像String类中真的有这个函数一样
}
是不是感觉很神奇?通过这种机制,我们可以将那些第三方类不具备的功能强行进行扩展,来方便我们的操作。
注意,类的扩展是静态的,实际上并不会修改它们原本的类,也不会将新成员插入到类中,仅仅是将我们定义的功能变得可调用,使用起来就像真的有一样。同时,在编译时也会明确具体调用的扩展函数:
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle" //虽然这里同时扩展了父类和子类的getName函数
fun printClassName(s: Shape) { //但由于这里指定的类型是Shape,因此编译时也只会使用Shape扩展的getName函数
println(s.getName())
}
fun main() {
printClassName(Rectangle())
}
由于类的扩展是静态的,因此在编译出现歧义时,只会取决于形参类型。
如果是类本身就具有同名同参数的函数,那么扩展的函数将失效:
class Test {
fun hello() = println("你干嘛")
}
fun Test.hello() = println("哎哟")
fun main() {
Test().hello() //你干嘛
}
不过,我们如果通过这种方式实现函数的重载,是完全没有问题的:
class Test {
fun hello() = println("你干嘛")
}
fun Test.hello(str: String) = println(str) //重载一个不同参数的同名函数
fun main() {
Test().hello("不错") //有效果
}
同样的,类的属性也是可以通过这种形式来扩展的,但是有一些小小的要求:
可以看到直接扩展属性是不允许的,前面我们说过,扩展并不是真的往类中添加属性,因此,扩展属性本质上也不会真的插入一个成员字段到类的定义中,这就导致并没有变量去存储我们的数据,我们只能明确定义一个getter和setter来创建扩展属性,才能让它使用起来真的像是类的属性一样:
val Student.gender: String
get() = "666"
fun main() {
val stu = Student()
println(stu.gender)
}
由于扩展属性并没有真正的变量去存储,而是使用get和set函数来实现,所以,像前面认识的field这种后备字段,就无法使用了。
还有一个需要注意的时,我们在不同包中定义的扩展属性,同样会受到访问权限控制,需要进行导入才可以使用:
import com.test.gender
fun main() {
val stu = Student()
println(stu.gender)
}
除了直接在顶层定义类的扩展之外,我们也可以在一个类中定义其他类的扩展,并且在定义时可以直接使用其他类提供的属性:
class A {
fun hello() = "Hello World"
}
class B {
fun A.test() {
hello() //直接在类A的扩展函数中调用A中定义的函数
}
}
像这种扩展,由于是在类中定义,因此也仅限于类内部使用,比如:
class A {
fun hello() = "Hello World"
}
class B (private val a: A){
private fun A.test() = hello() + "!!!"
fun world() = println(a.test()) //只能在类中通过A的实例使用扩展函数
}
fun main() = B(A()).world()
扩展属性无法访问那些本就不应该被当前作用域访问的类属性,即使它是对某个类的扩展,比如下面这种情况:
在名称发生冲突时,需要特别处理:
class A {
fun hello() = "Hello World"
}
class B (private val a: A){
private fun A.test() {
hello() //直接使用优先匹配被扩展类中的方法
this.hello() //扩展函数中的this依然指的是被扩展的类对象
this@B.hello() //这里调用的才是下面的
}
fun hello() = "Bye World"
}
定义在类中的扩展也可以跟随类的继承结构,进行重写:
open class A {
open fun Student.test() = "AAA"
fun hello() = println(Student().test())
}
class B : A() {
override fun Student.test() = "BBB" //对父类定义的扩展函数进行重写
}
fun main() {
A().hello()
B().hello()
}
局部扩展也是可以的,我们可以在某个函数里面编写扩展,但作用域仅限于当前函数:
fun main() {
fun String.test() = ""
"".test()
}
如果我们将一个扩展函数作为参数给到一个函数类型变量,那么同样需要再具体操作之前增加类型名称才可以:
fun main() {
//因为是对String类型的扩展函数,需要String.前缀
val func: String.() -> Int = {
this.length //跟上面一样,扩展函数中的this依然指的是被扩展的类对象
}
println("sahda".func()) //可以直接对符合类型的对象使用这个函数
func("Hello") //如果是直接调用,那么必须要传入对应类型的对象作为首个参数,此时this就指向我们传入的参数
}
可以看到,此函数的类型是 String.() -> Int
,也就是说它是专门针对于String类型编写的扩展函数,没有参数,返回值类型为Int,并使用Lambda表达式进行赋值,同时这个函数也是属于String类型的,只能由对象调用,或是主动传入一个相同类型的对象作为参数才能直接调用。可能这里会有些绕不太好理解,需要同学们多去思考。
总结一下,扩展属性更像是针对于原本类编写的外部工具函数,而绝不是对原有类的修改。
评论区