持续创作,加速成长!这是我参与「掘金日新计划 · 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,代表次构造器,但这时添加这个怪怪的,因为都没有主构造器,编译器会建议你把次构造器改为主构造器
因此,次构造器通常还是在主构造器出现后继续补充的,但是有了主构造器,可就不能这么写了,编译会提示需要调用主构造器,这其实是需要委托
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里面的内容
而如果既没有主构造也没有次构造,那么会提供空的主构造
class Cat { // 会默默安排一个()
}
就相当于
class Cat() {
}
4. 实例化
了解了构造函数,那么其实实例化可以看成就是去调用构造函数,只不过Kotlin没有new关键字,直接省略了new
val c = Cat() // 没有new,()根据构造器的参数列表选择,需要对应
更加简洁,这样就实例化一个类的对象
5. 继承
在Kotlin当中,每一个类都是Any类的子类,这和Java中的Object类很是类似
整体结构很简单,提供这3个方法,也很眼熟,适用于每个类型
5.1. 继承定义
默认的情况下,所有的Kotlin类都是final的
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将调用过程委托给了父类去抉择
回顾一下,类的次构造函数所起的作用似乎是将构造函数进行了二次封装,以适用某些特定的场景
而在继承的关系中,子类的主次构造直接去关联父类的对应构造(这里直接根据参数进行匹配),然后父类会重复前面类中的过程,由内部的链组合成了代代相传的继承链
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关键字对被覆写的方法进行标注,并且编译器可以对此进行一次检验
标记为override默认本身是open,那么它可以被下一级的子类直接覆写
需要“自闭”可以使用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,因为var比val多出setter方法,或者就直接使用一样的范围,总之,覆写的属性的范围能够将原属性的内容完整装进来就可以了
open class Cat(open val name: String) { // 需要开放构造器参数
}
class AmericanShortHair(override val name: String, age: Int) : Cat(name) { // 对于主构造器参数覆写
}
也可以在构造器中使用override关键字声明,添加val和var对应的是使用属性对应初始化的过程,这是一种简化写法,这里通过这样的方式关联覆写父类对应的属性
5.4. 父子类的初始化顺序
其实,通过先前对于子类的构造函数流程的分析,大致能够理出一点脉络,那么对于整体初始化顺序的拿捏,还是通过代码的验证更为直接
open class Cat(name: String) {
init {
println("父类初始化")
}
}
class AmericanShortHair(name: String, age: Int) : Cat(name) {
init {
println("子类初始化")
}
}
初始化块和属性初始化器几乎是一样的,接下来构建子类的对象
val cat = AmericanShortHair("小黄", 10)
父类先于子类进行初始化
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的属性,因为构造的先后顺序会造成种种问题编译器也会进行提示
在自己设计类的时候需要多多梳理,才能保证逻辑通畅,使得最终编写的程序更加健壮