自学内容网 自学内容网

六、封装、继承和多态&类的扩展

封装、继承和多态是面向对象编程的三大特性。

  • 封装,把对象的属性和函数结合成一个独立的整体,隐藏实现细节,并提供对外访问的接口。
  • 继承,从已知的一个类中派生出一个新的类,叫子类。子类实现了父类所有非私有化的属性和函数,并根据实际需求扩展出新的行为。
  • 多态,多个不同的对象对同一消息作出响应,同一消息根据不同的对象而采用各种不同的函数。

正是这三大特性,能够让我们的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函数,那么会直接对等号两边的变量进行引用判断===判断是否为同一个对象。

实际上在我们重写类的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关键字然后添加尖括号指定对应接口,并手动调用接口对应函数
    }
}

对于接口,我们可以像之前一样,将变量的类型设定为一个接口的类型,当做某一个接口的实现来使用,同时也支持isas等关键字进行类型判断和转换:

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不加也只能在类中访问A
    private fun A.test() = hello() + "!!!"
    fun world() = println(a.test())   //只能在类中通过A的实例使用扩展函数
}

fun main() = B(A()).world()//使用A().test()报错

扩展属性无法访问那些本就不应该被当前作用域访问的类属性,即使它是对某个类的扩展,比如下面这种情况:
在这里插入图片描述
在名称发生冲突时,需要特别处理:

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类型的,只能由对象调用,或是主动传入一个相同类型的对象作为参数才能直接调用。可能这里会有些绕不太好理解,需要同学们多去思考。

总结一下,扩展属性更像是针对于原本类编写的外部工具函数,而绝不是对原有类的修改。


原文地址:https://blog.csdn.net/m0_63790435/article/details/142717302

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!