Kotlin语法基础篇六:类与继承,你真的掌握了吗?

前言

上一篇文章我们介绍了Kotlin中扩展函数和高阶函数在实际开发中的运用。这一篇文章我们继续讲解Kotlin中的基础知识类与继承。关于接口、单例类、数据类、密封类、枚举类计划将作为单独文章来写,因为Kotlin中的类有很多细节方面的知识点,放在一篇文章来写感觉很难介绍清楚。下面我们开始本篇文章的学习。

1.类

在Kotlin中我们想要定义一个类,和Java中一样都是使用关键字class。如下示例代码,我们定义一个Person类:

arduino 复制代码
//    类名   类头               类体
class Person(var name: String) {  }

类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头与类体都是可选的; 如果一个类没有类体,可以省略花括号。

kotlin 复制代码
class Person

2.构造函数

在Kotlin中构造函数分为主构造函数次构造函数 。一个类可以有多个次构造函数和一个主构造函数,主构造函数是类头的一部分,紧跟在类名后,使用construtor关键字来表示:

javascript 复制代码
class Person constructor(var name: String) {  }

如果主构造函数没有任何注解或者可见性修饰符,可以省略这个constructor关键字。

arduino 复制代码
class Person(var name: String) { }

如果一个类有注解或者访问修饰符,construtor关键字是不可以省略的,且注解或者访问修饰符需要放在constructor关键字的前面。比如我们想让Person类的主构造函数声明为私有的,或者我们需要给主构造函数添加注解,我们就可以这么写:

typescript 复制代码
// 私有化主构造函数
class Person private constructor(var name: String) { }

// 给构造函数添加注解
class Person @Inject constructor(var name: String) { }

// 给构造函数添加注解同时私有化构造函数
class Person @Inject private constructor(var name: String) { } 

3.init初始化块

在Java语言中我们可以在一个类的任意构造函数中去写一些初始化的逻辑。而在Kotlin中我们的主构造函数不能包含任何代码(次构造函数可以),初始化的代码需要放到init关键字作为前缀的初始化块中,如下代码示例:

kotlin 复制代码
class Person(var name: String) {
    init {
        println("init person class")
    }
}

如果你刚开始接触Kotlin对于这种写法可能会很不习惯。因为我们把原本放在构造函数里的初始化逻辑,放到了init块中。当然我觉得这和Kotlin声明主构造函数的语法有关,试想一下我们已经有了一个类体的花括号{},如果再加上一个主构造函数的方法体花括号{},那么这种写法看上去太不协调了。所以把主构造函数的初始化逻辑放到了单独的init块中是合理的。 init块中的代码是主构造函数的一部分,它将和类属性的初始化按照他们在类体中出现的顺序执行,与属性初始化交替在一块,如下示例代码:

kotlin 复制代码
class Person(var name: String) {

    var height = 170.also { println("height = $it,name = $name") }

    init {
        println("class person init one, $height, name = $name")
    }

    var age = 18.also { println("age = $it, name = $name") }

    init {
        println("class person init two, $age, name = $name")
    }

}

fun main() {
    val person = Person("Jack")
}

// 输出
height = 170,name = Jack
class person init one, 170, name = Jack
age = 18, name = Jack
class person init two, 18, name = Jack

而构造函数中的参数是可以在类属性的初始化器和init块中访问的。上面的示例代码中为了证明这一特性,特意在打印的地方增加了参数name的打印。

4.在主构造函数中声明属性

属性和控制流的文章中我们介绍了在Kotlin中声明一个属性使用关键字valvar。当我们在构造函数中给一个参数加上valvar关键字的时候就代表我们将这个参数定义成了该类的属性。没错,这个参数即使是这个构造函数的参数也是这个类的属性。

上面我们说到构造函数中的参数可以在类属性的初始化器和init块中访问,而我们将参数声明成属性的时候我们就可以在该类的方法中去访问该参数了,如果不声明成属性,我们是没有办法在该类的方法中去访问该参数的,如下示例代码:

构造函数中的参数无法在该类中的方法中访问。

将该参数声明成类属性我们就可以在该类的方法中访问该属性。这里比较特殊的是,通常我们声明一个类的属性都是放在类体中去声明的,而在Kotlin中给我们提供了更便捷的语法,我们可以在该类的主构造函数中来声明一个类属性,同时该属性也是这个主构造函数的参数。

5.次构造函数

我们也可以在一个类的类体中声明次构造函数,一个类可以有多个次构造函数:

kotlin 复制代码
class Person {
    constructor(name: String) { println("name = $name") }
    constructor(name: String, age: Int) { println("name = $name, age = $age") }
}

当一个类既有主构造函数 又有次构造函数 的时候,每个次构造函数需要委托给主构造函数,可以直接委托或者通过别的次构造函数委托。委托到同一个类的另一个构造函数使用关键字this:

kotlin 复制代码
class Person(name: String) {
    
    // 直接委托
    constructor(name: String, age: Int) : this(name) {
        println("name = $name, age = $age")
    }
    
    // 间接委托
    constructor(name: String, age: Int, sex: Int) :this(name, age) {
        println("name = $name, age = $age, sex = $sex")
    }

}

上面我们说到初始化块中的代码会作为主构造函数中的一部分,委托给主构造函数init块中的代码会作为次构造函数的第一条语句,因此所有初始化块与属性初始化器中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块:

kotlin 复制代码
class Person(name: String) {

    val info = name.apply { println("info init: name = $name") }

    init {
        println("person class init: name = $name")
    }

    constructor(name: String, age: Int) : this(name) {
        println("name = $name, age = $age")
    }

    constructor(name: String, age: Int, sex: Int) :this(name, age) {
        println("name = $name, age = $age, sex = $sex")
    }

}

fun main() {
    val person = Person("Jack", 20)
}

// 输出
info init: name = Jack
person class init: name = Jack
name = Jack, age = 20

6.创建类的实例

在Kotlin中创建一个对象的实例省略了new关键字:

ini 复制代码
val person1 = Person("Jack")
val person2 = Person("Jack", 20)

在等号后直接调用该类对应的构造函数即可。而调用一个类的构造函数使用ClassName()的语法,()内传入该构造函数对应的参数。这和普通的函数调用很类似,只是我们将函数名替换成了类名。

7.继承

在Java中所有类都有一个共同的超类Object,而在Kotlin中这个超类是Any,对于没有超类型声明的类默认超类都是Any。Any中有三个方法:equals、hashcode、toString。因此为所有的类都定义了这些方法。下面贴出Any类的源码,为了方便阅读,把源码中的注释做了删减。

kotlin 复制代码
public open class Any {
    public open operator fun equals(other: Any?): Boolean

    public open fun hashCode(): Int

    public open fun toString(): String
}

class Person // 隐式继承Any

默认情况下,Kotlin中的类都是不可被继承的。要使一个类可被继承使用关键字open修饰,放在关键字class之前。

arduino 复制代码
open class Person // Person类可被继承

在Java中我们遵循单继承多实现的原则,继承一个类使用关键字extends,实现一个接口我们使用关键字implements。而在Kotlin中我们也要遵循这个原则,只是继承和实现我们都使用冒号:来表示,在类头中把超类型放在冒号之后,多个超类用逗号隔开,如下示例代码:

arduino 复制代码
open class Person(name: String) { }

class Student(name: String): Person(name) { }

interface Callback { }

class Student(name: String): Person(name), CallBack { } 

如果子类有一个主构造函数,其父类可以(并且必须)使用子类主构造函数中的参数就地初始化。写到这里,你没有发现我们在Koltin类中继承另外一个类的时候总是需要在父类的后面添加一对小括号(),比如我们在继承一个Activity的时候:

kotlin 复制代码
class MainActivity : AppCompatActivity() { }

而在Java中我们就不需要添加这对小括号,这是因为Java和Kotlin在语法上的差异,但是它们都必须遵循类的继承原则,子类的构造函数必须访问父类的构造函数。如果你不给一个类显示的声明一个构造函数,那么该类将默认拥有一个无参的构造函数。而在Kotlin中子类访问父类的无参的构造函数在继承时必须要添加上小括号来表示()。而当一个类只有次构造函数的时候,我们不需要在继承的时候就去访问父类的无参构造函数,而是在次构造函数去访问,这时我们就可以省略这个(),如下示例代码:

kotlin 复制代码
class Person : Any {
    constructor() : super()
}

fun main() {
    val person = Person()
}

我们使用次构造函数去访问父类的无参构造函数。再举一个实际开发中比较常见的例子,我们使用java代码去自定义一个View:

less 复制代码
class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

在Android Studio中选中当前类然后右击,在弹窗中选择Convert Java File to Kotlin File: 如下图中,转换成Kotlin的MyView中并没有主构造函数,只有三个次构造函数。我们在创建MyView实例的时候在次构造函数中去访问父类中的构造函数,声明MyView类的时候自然就不再需要小括号了()。

8.覆盖方法

要使一个类可被继承我们需要在类声明时的class关键字前加上open关键字。而要使一个方法可被覆盖,我们依然使用open关键字,我们只需要在声明该方法的fun关键字前加open即可。如下代码示例,我们给Person类添加一个用open关键字修饰的eat()方法:

kotlin 复制代码
open class Person constructor(name: String) {
    open fun eat() {
        println("eat method called")
    }
}

class Student(var name: String) : Person(name) {
    override fun eat() {
        super.eat()
    }
}

覆盖一个方法必须要使用override关键字来修饰。标记为override的成员本身是开放的,可以在子类中覆盖它。如果你想禁止再次覆盖,使用final关键字:

kotlin 复制代码
class Student(var name: String) :Person(name) {
    final override fun eat() {
        super.eat()
    }
}

9.覆盖属性

覆盖属性与覆盖方法类似,在父类中声明的属性在子类中重新声明必须以override开头:

kotlin 复制代码
open class Person {
    open val name: String = "Any"
}

class Student : Person() {
    override var name: String = "Jack" 
}

我们可以使用var属性覆盖一个val属性,如上述示例。但反之却不行,因为一个val属性本质上声明了一个get方法,而将其覆盖为var只是在子类中额外声明了一个set方法。上面我们介绍到可以在一个类的主构造函数中将参数声明成属性,而我们也可以将一个需要覆盖的属性声明在主构造函数中,如下示例代码:

kotlin 复制代码
open class Person {
    open val name : String = "Any"
}

class Student constructor(override var name: String) : Person() { }

10.子类的初始化顺序

如果一个类拥有父类,那么在创建该类的时候会先完成父类的初始化。遵循面向对象的继承原则,子类的构造函数必须访问父类的构造函数,如下代码示例:

kotlin 复制代码
open class Person {

    init {
        println("parent class init")
    }

    open val name : String = "Any".also { println("parent name = $it") }

}

class Student constructor(override var name: String) : Person() {

    init {
        println("child class init: name = $name")
    }

    private val age = 20.also { println("child age = $it") }

}

fun main() {
    val student = Student("Jack")
}

// 输出
parent class init
parent name = Any
child class init: name = Jack
child age = 20

11.嵌套类和内部类

  • 嵌套类:我们可以在一个类或者一个接口的内部声明一个嵌套类。如下示例代码:
kotlin 复制代码
class Outer {
    init { println("Outer init")  }
    class A { init { println("Class A init") } }
}

interface CallBack {
    class B { init { println("Class B init") } }
}

fun main() {
    val a = Outer.A()
    val b = CallBack.B()
}

// 输出
Class A init
Class B init

创建一个嵌套类的对象需要依赖于他的外部类,即在调用一个内部类的构造函数前添加OuterClassName.。需要注意的是在嵌套类中是无法去访问外部类的属性和方法的。

  • 内部类 :在Kotlin中我们使用inner关键字来定义一个内部类,内部类的创建需要先创建外部类的实例,内部类可以访问外部类的属性和方法:如下示例代码:
kotlin 复制代码
class Outer {
    init { println("Class Outer init")  }

    val name = "Outer"

    fun getInfo() = "info"

    inner class A {
        init { println("Class A init") }

        val info = "$name - ${getInfo()}".also { println("info = $it") }
    }
}

fun main() {
    Outer().A()
}

// 输出
Class Outer init
Class A init
info = Outer - info

12.访问父类中的方法和属性

在子类中可以使用super关键字调用其父类的函数与属性访问器的实现:

kotlin 复制代码
open class Person {
    open fun eat() {
        println("Cooking time")
    }
    open val work get() = "study work hard"
    val sleep = 8 * 60 * 1000L
}

class Student : Person() {
    override fun eat() {
        super.eat()
    }
    override val work = super.work
    val mySleep = super.sleep
}

在一个内部类中如果想要访问外部类父类中的属性和方法,使用@标签限制的super语句:

kotlin 复制代码
open class Person {
    open fun eat() {
        println("Cooking time")
    }
    open val work get() = "study work hard"
    val sleep = 8 * 60 * 1000L
}

class Student : Person() {
    override fun eat() {
        super.eat()
    }
    override val work = super.work
    val mySleep = super.sleep

    inner class A {
        val work = super@Student.work
        fun getInfo() {
            super@Student.eat()
        }
    }
}

13.覆盖多个父类中的同名方法

如果一个类它有多个父类,并且它的多个父类中有同名的方法。我们在重写该方法时,若要在该方法内去访问父类中的同名方法,我们使用由尖括号中父类型名限定的super,如下代码示例:

kotlin 复制代码
open class Person {
    open fun eat() {
        println("Cooking time")
    }
}

interface CallBack {
    fun eat() { } // Kotlin接口中的方法可以有默认实现
}

class Student : Person(),CallBack {
    override fun eat() {
        super<Person>.eat()
        super<CallBack>.eat()
    }
}

14.抽象类

和Java中一样,定义一个抽象类我们使用关键字abstract,抽象类默认就是可继承的。将abstract关键字放在class关键字前面:

csharp 复制代码
abstract class BaseFragment() { }

抽象类和接口的区别在于抽象类既可以定义抽象方法和属性,也可以定义非抽象的方法和属性:

kotlin 复制代码
abstract class Person {
    abstract fun eat()
    abstract val name: String
    fun work() { println("hard work") }
    val info = "person class"
}

class Student : Person() {
    override fun eat() {
        println("Cooking time")
    }
    override val name = "Jack"
}

15.Class和File的区别

在Kotlin中我们可以不用依靠类的作用域,来创建一个属性和方法。通常我们会将顶层属性、扩展函数、顶层函数放在一个File文件中,这样我们就可以在任意需要的地方去访问它们。或者我们需要在一个文件中定义多个类或者接口,我们都可以直接定义成File。如果仅仅是普通的类,我们直接定义成Class文件就好。当我们在一个Kotlin类的作用域外部,添加任意的属性、方法、接口、类,Kotlin编译器会自动识别将Class文件转换成.kt的File文件。

总结

其实类与继承相对于高阶函数和Lambda表达式是更容易理解一点。但是这其中密密麻麻的小知识点,在我们实际开发中都是会遇到的,我们不能只停留在代码搬运的层面上,而是要去理解它。这样我们才能在实际开发中灵活的运用它。这篇文章到这里就结束了,下篇文章我们继续讲解Kotlin中的基础知识接口,我们下期再见~

相关推荐
懒大王爱吃狼1 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
逐·風5 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
Devil枫5 小时前
Vue 3 单元测试与E2E测试
前端·vue.js·单元测试
尚梦6 小时前
uni-app 封装刘海状态栏(适用小程序, h5, 头条小程序)
前端·小程序·uni-app
GIS程序媛—椰子6 小时前
【Vue 全家桶】6、vue-router 路由(更新中)
前端·vue.js
前端青山7 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
毕业设计制作和分享7 小时前
ssm《数据库系统原理》课程平台的设计与实现+vue
前端·数据库·vue.js·oracle·mybatis
清灵xmf9 小时前
在 Vue 中实现与优化轮询技术
前端·javascript·vue·轮询
大佩梨9 小时前
VUE+Vite之环境文件配置及使用环境变量
前端
GDAL9 小时前
npm入门教程1:npm简介
前端·npm·node.js