Kotlin面向对象的基础(一)

68 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

1. 前言

其实自己使用Kotlin编写项目也有一段时间了,但是一直以来并没有系统的了解过文档,知识还不成体系,因而借助官方的文档和一些自己的实践来进一步提升,使自己对于Kotlin这门语言的使用更加趋于灵活

2. 类

Kotlin中的类依然使用class关键字进行声明,这点和Java是一样的

class Cat {
    // 这就是一个类,尽管空空如也
}

像这样一个类什么也没有,依然是一个类,由于它没有内容,因此多这对大括号也没什么用,索性拿走

class Cat

现在这就完全是一个标志了

3. 构造函数

构造函数的概念也不陌生,通常是在类进行实例化的时候进行调用,用来处理一些初始化的操作
然而,在Kotlin语言中有主构造函数次构造函数的概念,并且在一个类中可以同时存在一个主构造函数和若干个次构造函数

3.1. 主构造函数

主构造函数直接写在类名的后面,使用constructor关键字

class Cat constructor(name: String, breed: String)

构造函数也是函数,constructor接上参数列表,在类的主构造函数没有注解和可见性修饰符的情况下可以对constructor关键字进行省略,这就像平时没有客人来家里做客你可以只穿睡衣一样,简单舒服就好

class Cat(name: String, breed: String)   // 省略constructor关键字

主构造函数本身不包含代码,初始化的操作通常放置在init{}块中进行,与此同时,还包括属性初始化器,也就是对于类中的属性的直接赋值操作

class Cat(name: String, breed: String) {
    private var name: String = "修猫"  // 属性初始化器,直接赋值

    init {
        // 初始化块
        println("小猫出生……")
    }
}

并且,属性初始化器与init{}根据出现的先后进行执行,这就意味着二者的优先级应该是同等的,并且这二者都可以使用主构造函数的参数,毕竟主构造函数的参数的位置才是最靠前的

class Cat(name: String, breed: String) {

    private var name: String = name   // 取主构造函数的参数

    init {
        // 初始化块
        println("小猫出生……${this.name}")   // 特指类的属性name,并非传入的参数,值现在一样
    }
}

通常我们对于类的初始化大概是这样:

class Cat(name: String, breed: String) {

    private var name: String = ""
    private var breed: String = ""

    init {
        this.name = name
        this.breed = breed
    }
}

那么,对于这种比较常见写起来又稍显死板的初始化,Kotlin可以更加简洁

class Cat(var name: String, var breed: String) {  // 声明初始化一条龙
    // todo 直接开始正文吧
}

直接在对应的参数前面加上var或者val,根据需要而定,其实正是对应了原先内部的属性,接下来就可以直接编写内部的逻辑了

3.2. 次构造函数

声明类的次构造函数的时候,同样使用constructor关键字

class Cat {
    constructor(name: String) {   // 类的内部
        println(name)
    }
}

回到未添加主构造器的时候,这时在类的内部添加constructor,代表次构造器,但这时添加这个怪怪的,因为都没有主构造器,编译器会建议你把次构造器改为主构造器

因此,次构造器通常还是在主构造器出现后继续补充的,但是有了主构造器,可就不能这么写了,编译会提示需要调用主构造器,这其实是需要委托

image.png

class Cat() {
    constructor(name: String) : this() {  // 这代表委托给当前类的主构造,委托给一把手
        println(name)
    }
}

类就像一个组织,有层级有条理的,这么大的事二把手说了不算,还得过问一把手
前面也说了,一个类可以有一个主构造和多个次构造,那么,其他的次构造也得过问一把手,当然,这个时候可以是间接的,但是殊途同归

class Cat(name: String, breed: String) {
    constructor(name: String) : this(name, "美短加白") {  // 委托主构造
        println(name)
    }
    constructor(type: Int) : this(type.toString()) {   // 委托给上一个次构造,因为参数匹配
        println(type)
    }
}

把类整得复杂些,第一个次构造委托主构造进行创建,而第二个次构造则是委托第一个,this()在类中匹配可用的构造(不分主次)根据参数列表,它匹配第一个次构造,因此第二个次构造到主构造的桥梁也打通了,整体是一条链

主构造函数基于其地位(也就是链的顺序),保证在所有次构造之前执行,因此init{}和属性初始化器作为最先执行的部分,其他次构造根据委托的顺序执行

在较为特殊的情形下,比如没有主构造,仅有次构造,这种情况不能使用主构造实例化,但是init{}和属性初始化器的执行顺序依然是保证在最前面的

class Cat {
    init {     // 没主构造也最先
        println("初始化")
    }
    constructor(name: String) {
        println(name)
    }
}

如果实例化,是先执行init{},再执行constructor里面的内容

image.png

而如果既没有主构造也没有次构造,那么会提供空的主构造

class Cat {  // 会默默安排一个()
    
}

就相当于

class Cat() {

}

4. 实例化

了解了构造函数,那么其实实例化可以看成就是去调用构造函数,只不过Kotlin没有new关键字,直接省略了new

val c = Cat()  // 没有new,()根据构造器的参数列表选择,需要对应

更加简洁,这样就实例化一个类的对象

5. 继承

在Kotlin当中,每一个类都是Any类的子类,这和Java中的Object类很是类似

image.png

整体结构很简单,提供这3个方法,也很眼熟,适用于每个类型

5.1. 继承定义

默认的情况下,所有的Kotlin类都是final

image.png

final代表的就是它们不能被继承,就像末代皇帝一样,因为没有后续了,所以成为最终的
如果要想被其他类继承,需要用到open关键字

open class Cat {  // 开放继承
    
}

声明一个类继承Cat类,AmericanShortHair就成为Cat类的子类,后面需要使用父类的构造函数

class AmericanShortHair : Cat() {
    
}

刚刚没有定义主构造函数,默认会使用对应的无参构造,现在定义一个带有参数的

class AmericanShortHair(name: String, age: Int) : Cat() {
    
}

现在进行实例化时,子类需要对应的参数,而如果父类需要构造器的参数,有一些限制

open class Cat(name: String) {   // 构造器参数是子类的子集

}

class AmericanShortHair(name: String, age: Int) : Cat(name) {  // 父类仅接受子类的参数

}

父类的参数列表内容不能超出子类的参数列表的范围,毕竟构造的是子类的对象,参数是由子类给过来的,父类本身不另外定义,仅从子类的参数中进行获取

open class Cat(name: String, age: Int) {

}

class AmericanShortHair : Cat {   // 没有定义主构造
    constructor(name: String) : super(name, 10)   // 代表调用父类的构造,与先前的this对照着看
    
    constructor(name: String, age: Int) : super(name, age)
}

和之前使用次构造器需要最终用到this指向主构造一样,只不过这里改为super指向父类的构造(不区分主次),这里最终实际上也构成了,实际上通过super将调用过程委托给了父类去抉择

image.png
回顾一下,类的次构造函数所起的作用似乎是将构造函数进行了二次封装,以适用某些特定的场景

image.png
而在继承的关系中,子类的主次构造直接去关联父类的对应构造(这里直接根据参数进行匹配),然后父类会重复前面类中的过程,由内部的链组合成了代代相传的继承链

5.2. 方法覆写

Kotlin同样也会有方法覆写的过程,这正是由共性走向个性的过程

open class Cat(name: String, age: Int) {
    open fun eat() {    // 开放才能修改
        println("干饭干饭")
    }
}

如果需要覆写父类中的方法,前提是这个方法被标注为open,在Kotlin中方法默认会被标注为final,即不可被覆写的,只属于类自身,因此需要显式开放

class AmericanShortHair(name: String, age: Int) : Cat(name, age) {
    override fun eat() {      // 使用override覆写
        println("美短在干饭")
    }
}

子类中使用override关键字对被覆写的方法进行标注,并且编译器可以对此进行一次检验

image.png

标记为override默认本身是open,那么它可以被下一级的子类直接覆写

image.png

需要“自闭”可以使用final修饰,这样开放的范围便到此为止

class AmericanShortHair(name: String, age: Int) : Cat(name, age) {
    final override fun eat() {    // 下一级不能覆写
        println("美短在干饭")
    }
}

5.3. 属性覆写

属性的覆写与方法的覆写具有一定的相似性

open class Cat(name: String, age: Int) {

    open val color = ""  // 默认也是final,需要open
}
class AmericanShortHair(name: String, age: Int) : Cat(name, age) {

    override var color = "黑白灰"    // 覆写使用override
}

对照方法的覆写,父类的属性需要使用open修饰,默认情况也是final,其后在子类中进行覆写时同样采用override关键字

需要注意的就是子类的属性要能够兼容父类的属性,更直白一点就是能够接受它,或者说包含的范围更大
举个例子,子类可以用var接收val,因为varval多出setter方法,或者就直接使用一样的范围,总之,覆写的属性的范围能够将原属性的内容完整装进来就可以了

open class Cat(open val name: String) {   // 需要开放构造器参数
    
}

class AmericanShortHair(override val name: String, age: Int) : Cat(name) {  // 对于主构造器参数覆写

}

也可以在构造器中使用override关键字声明,添加valvar对应的是使用属性对应初始化的过程,这是一种简化写法,这里通过这样的方式关联覆写父类对应的属性

5.4. 父子类的初始化顺序

其实,通过先前对于子类的构造函数流程的分析,大致能够理出一点脉络,那么对于整体初始化顺序的拿捏,还是通过代码的验证更为直接

open class Cat(name: String) {
    init {
        println("父类初始化")
    }
}

class AmericanShortHair(name: String, age: Int) : Cat(name) {
    init {
        println("子类初始化")
    }
}

初始化块和属性初始化器几乎是一样的,接下来构建子类的对象

val cat = AmericanShortHair("小黄", 10)

image.png

父类先于子类进行初始化

open class Cat(name: String) {

    open val age = "20"   // 被子类覆盖

    init {
        println("父类初始化$age")
    }
}

class AmericanShortHair(name: String, age: Int) : Cat(name) {
    override val age = "67"
    init {
        println("子类初始化")
    }
}

这种情况下,父类的输出age会为null,因为在父类初始化之时,age还没有值,用来覆盖的age要在子类初始化时构造,因此这是一种需要避免的写法,需要避免在构造函数中使用open的属性,因为构造的先后顺序会造成种种问题编译器也会进行提示

image.png

在自己设计类的时候需要多多梳理,才能保证逻辑通畅,使得最终编写的程序更加健壮

类与继承 - Kotlin 语言中文站 (kotlincn.net)