Kotlin 类的创建与继承

Kotlin 官方在线编程网址:


本文参考自:


基本写法

创建

通过 class 关键字创建一个类:

1
2
3
4
5
6
7
8
class Person {
var name = ""
var age = 0

fun eat() {
println("$name is eating.")
}
}

实例化方式:

1
val person = Person()

继承

默认所有非抽象类都是不可以被继承的。

如果需要将其变为父类,则需要在 class 前使用 open 关键字。

1
2
3
4
5
6
7
8
open class Person {
var name = ""
var age = 0

open fun eat() {
println("$name is eating.")
}
}

如果需要继承另一个类,则使用 : 符号:

1
2
3
class Student : Person() {
var grade = 0
}

重写

重写父类的方法

需要注意的是如果父类的方法没有 open 声明,那么该方法是不允许被重写的。重写方法的时候需要使用 override 关键字注明。还有一点就是子类可以选择不重写父类的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
open class Dog {
fun shout() {
println("汪汪")
}
open fun show() {
println("萌萌哒")
}
}

class Husky : Dog() {
// 由于父类的shout方法没有写open, 也就是默认是final的, 所以这里不能重写shout方法
// Error:(15, 5) Kotlin: 'shout' in 'Dog' is final and cannot be overridden 不能重写 shout 因为默认是 final 的
/* override fun shout() {
println("汪汪汪")
}*/

// 重写父类方法的时候要加上override关键字, show方法也可以选择不进行重写
override fun show() {
println("喵喵")
}
}

fun main(args: Array<String>) {
val husky = Husky()
husky.shout()
husky.show()
}

再次需要注意的是,如果一个类集成了多个类/接口,并且父类/接口中有相同名字的方法需要重写的时候,那么子类这时候必须重写该方法,并且如果子类想区分开父类的方法的时候,可以使用 super 关键字调用不同父类的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
open class Dog {
fun shout() {
println("汪汪")
}
open fun show() {
println("萌萌哒")
}
}

interface WatchDog {
// interface里不用写open, 因为默认就是open的
fun show() {
println("看门电子狗")
}
}

// 使用接口的时候不用写(), 是因为接口是没有构造函数的
class Husky : Dog(), WatchDog {
// 由于父类的shout方法没有写open, 也就是默认是final的, 所以这里不能重写父类的方法
// Error:(15, 5) Kotlin: 'shout' in 'Dog' is final and cannot be overridden 不能重写shout因为默认是final的
/*override fun shout() {
println("汪汪汪")
}*/

// 重写父类方法的时候要加上override关键字
override fun show() {
// 当重写的方法在父类中有多个实现的时候, 如果继承的多个类中, 有相同的方法需要重写的时候, 则子类必须重写该方法, 并且, 为了区分, 可以使用super关键字来调用不同的父类中的方法
super<WatchDog>.show() // 看萌电子狗
super<Dog>.show() // 萌萌哒
}
}

fun main(args: Array<String>) {
val husky = Husky()
husky.shout() // 汪汪
husky.show()
}

重写父类的变量

父类变量的重写的时候有几个需要注意的地方:

  1. 被重写的变量也要有 open 的声明。

  2. 子类可以使用 var 类型的变量去重写父类 val 类型的变量,但是不能使用 val 类型的变量去重写父类 var 类型的变量。

    如果使用 val 类型的变量去重写父类的 var 类型的变量, 那么子类这个 val 类型的变量会多一个 set 方法, 而 val 类型的变量是不允许有 set 方法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
open class A {
open val x: Int get() {
return 0
}

open var y : String = "a"
}

class B : A() {
override val x: Int = 1

override var y: String = "b"
}

class C : A() {
override var x : Int = 2

override val y : String = "c" // Error: Var-property public open val y: String defined in C cannot be overridden by val-property public open var y: String defined in A

}

构造函数

Kotlin 将构造函数分为了两种:主构造函数和次构造函数。

主构造函数

主构造函数是最常使用的构造函数,每个类默认都会有一个不带参数的主构造函数。

(特殊情况:如果在没有实现主构造函数的情况下实现了次构造函数,则会失去该不带参数的主构造函数。)

当然也是可以显式地为其指明参数。主构造函数的特点就是没有函数体,直接定义在类名的后面即可:

1
2
3
4
5
class Student(val studentNo: String, var grade: Int) : Person() {
}

// 或者可以写成
// class Student constructor(val studentNo: String, var grade: Int) : Person() {}

我们在实例化的时候需要传入构造函数中的所有的参数:

1
val student = Student("123456789", 3)

由于主构造函数没有函数体,所以我们无法直接在其中编写一些逻辑。但是 Kotlin 给我们提供了一个 init 结构体,所有主构造函数的逻辑都可以写在里面:

1
2
3
4
5
class Student(val studentNo: String, var grade: Int) : Person() {
init {
println("Student No is $studentNo AND grade is $grade.")
}
}

根据 Java 语言继承特性中的一个规定:子类的构造函数必须调用父类中的构造函数。这个规定在 Kotlin 中也要遵守。

所以在上文中 class Student : Person() 以及 class Student(val studentNo: String, var grade: Int) : Person() 中 Person 类后面的一对空括号表示 Student 类的主构造函数在初始化的时候会调用 Person 类的无参数构造函数,即使在自身的主构造函数为默认的无参数的时候也是不能省略的。

现在我们将 Person 类改造一下,将姓名和年龄都放到主构造函数中,即:

1
2
3
4
5
open class Person(val name: String, var age: Int) {
fun eat() {
println("$name is eating.")
}
}

因为我们自定义了主构造函数,所以默认提供的无参数的主构造函数就不存在了,所以我们也需要修改一下 Student 类的表示方法:

1
2
3
4
5
6
7
class Student(val studentNo: String, var grade: Int, name: String, age: Int) : Person(name, age) {

init {
println("Student No is $studentNo AND grade is $grade.")
}

}

注意,我们在 Student 类的主构造函数中增加 name 和 age 这两个字段时,不能再将它们声明成 val,因为在主构造函数中声明成 val 或者 var 的参数将自动成为该类的字段,这就会导致和父类中同名的 name 和 age 字段造成冲突。因此,这里的 name 和 age 参数前面我们不用加任何关键字,让它的作用域仅限定在主构造函数当中即可 。

现在就可以通过如下的代码来创建一个 Student 类的实例并使用了:

1
val student = Student("123456789", 3, "LiHua", 20)

次构造函数

其实你几乎是用不到次构造函数的,Kotlin 提供了一个给函数设定参数默认值的功能,基本上可以替代次构造函数的作用。

任何一个类只能有一个主构造函数,但是可以有多个次构造函数。次构造函数也可以用于实例化一个类,这一点和主构造函数没有什么不同,只不过它是有函数体的。

Kotlin 规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。

比如以下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
open class Person(val name: String, var age: Int) {}

class Student(name: String, age: Int, val studentNo: String, var grade: Int) : Person(name, age) {
init {
println("name : $name AND age : $age AND studentNo : $studentNo AND grade : $grade")
}

constructor(name: String , age: Int) : this(name, age, "12345", 2) {}

constructor() : this("XiaoMei", 19, "1234567", 1) {} // 如果 {} 内没有内容的话,可以省略大括号
}

fun main() {
val student1 = Student("LiHua", 20, "123456", 3)
val student2 = Student("XiaoMing", 21)
val student3 = Student()
}

// 运行结果
name : LiHua AND age : 20 AND studentNo : 123456 AND grade : 3
name : XiaoMing AND age : 21 AND studentNo : 12345 AND grade : 2
name : XiaoMei AND age : 19 AND studentNo : 1234567 AND grade : 1

特殊情况:

那么接下来我们就再来看一种非常特殊的情况:类中只有次构造函数,没有主构造函数。

这种情况真的十分少见,但在 Kotlin 中是允许的。

当一个类没有显式地定义主构造函数且定义了次构造函数时,它就是没有主构造函数的。

我们结合代码来看一下:

1
2
3
4
class Student : Person {
constructor(name: String, age: Int) : super(name, age) {
}
}

注意这里的代码变化,首先 Student 类的后面没有显式地定义主构造函数,同时又因为定义了次构造函数,所以现在 Student 类是没有主构造函数的。那么既然没有主构造函数,继承 Person 类的时候也就不需要再加上括号了(因为主构造函数会调用父类的构造函数)。

另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将 this 关键字换成了 super 关键字,这部分就很好理解了,因为和 Java 比较像,也就不再多说了。


函数的命名参数以及参数默认值

类的构造函数同样适用于命名参数以及参数默认值,所以我们在极大多数的情况下通过这两个特性配合主构造函数来可以实现次构造函数的功能。

命名参数

简单理解,就是它允许我们在调用函数的时候传入“形参的名字”。

如同以下形式:

1
helloFunction(name = "Kotlin")

让我们看一个更具体的使用场景:

1
2
3
4
5
6
7
8
9
10
11
fun createUser(
name: String,
age: Int,
gender: Int,
friendCount: Int,
feedCount: Int,
likeCount: Long,
commentCount: Int
) {
//..
}

我们可以通过如下的方法使用函数:

1
2
3
4
5
6
7
8
9
createUser(
name = "Tom",
age = 30,
gender = 1,
friendCount = 78,
feedCount = 2093,
likeCount = 10937,
commentCount = 3285
)

可以看到,在这段代码中,我们把函数的形参加了进来,形参和实参用 = 连接,建立了两者的对应关系。对比前面 Java 风格的写法,这样的代码可读性更强了。如果将来你想修改 likeCount 这个参数,也可以轻松做到。这其实就体现出了 Kotlin 命名参数的可读性与易维护性两个优势。

参数默认值

而除了命名参数这个特性,Kotlin 还支持参数默认值,这个特性在参数较多的情况下同样有很大的优势:

1
2
3
4
5
6
7
8
9
10
11
fun createUser(
name: String,
age: Int,
gender: Int = 1,
friendCount: Int = 0,
feedCount: Int = 0,
likeCount: Long = 0L,
commentCount: Int = 0
) {
//..
}

我们可以看到,gender、friendCount、feedCount、likeCount、commentCount 这几个参数都被赋予了默认值。这样做的好处就在于,我们在调用的时候可以省很多事情。比如说,下面这段代码就只需要传 3 个参数,剩余的 4 个参数没有传,但是 Kotlin 编译器会自动帮我们填上默认值。

1
2
3
4
5
createUser(
name = "Tom",
age = 30,
commentCount = 3285
)

对于无默认值的参数,编译器会强制要求我们在调用处传参;对于有默认值的参数,则可传可不传。Kotlin 这样的特性,在一些场景下就可以极大地提升我们的开发效率。而如果是在 Java 当中要实现类似的事情,我们就必须手动定义“3 个参数的 createUser 函数”,或者是使用 Builder 设计模式。

在构造函数中的使用案例

通过写出如下的代码:

1
2
3
4
5
6
7
8
class Student(
name: String = "",
age: Int = 0,
val studentNo: String = "",
val grade: Int = 0
) : Person(name, age) {
//..
}

在给主构造函数的每个参数都设定了默认值之后,我们就可以使用任何传参组合的方式来对 Student 类进行实例化,当然也包含了上文中的两种次构造函数的使用场景。


函数的可见性修饰符

熟悉 Java 的人一定知道,Java 中有 public、 private、 protected 和 default(什么都不写)这4种函数可见性修饰符。

Kotlin中也有4种,分别是 public、 private、protected 和 internal,需要使用哪种修饰符时,直接定义在 fun 关键字的前面即可。

下面将详细介绍一下 Java 和 Kotlin 中这些函数可见性修饰符的异同。

  1. private 修饰符在两种语言中的作用是一模一样的,都表示只对当前类内部可见。

  2. public 修饰符的作用虽然也是一致的,表示对所有类都可见,但是在 Kotlin中 public 修饰符是默认项,而在 Java 中 default 才是默认项。前面我们定义了那么多的函数,都没有加任何的修饰符,所以它们默认都是 public 的。

  3. protected 关键字在 Java 中表示对当前类、子类和同一包路径下的类可见,在 Kotlin 中则表示只对当前类和子类可见。

  4. Kotlin 抛弃了 Java 中的 default 可见性(同一包路径下的类可见),引人了一种新的可见性概念,只对同一模块中的类可见,使用的是 internal 修饰符。比如我们开发了一个模块给别人使用,但是有一些函数只允许在模块内部调用,不想暴露给外部,就可以将这些函数声明成 internal 。

image

所以,我们可以通过 private 关键字将类的构造函数定义为私有属性:

1
2
3
4
5
6
7
8
9
10
11
// 私有的主构造函数
class Student private constructor(
val studentNo: String,
var grade: Int
) : Person() {

// 私有的次构造函数
private constructor(grade: Int) : this("123", grade) {
}

}

Kotlin 类的创建与继承
https://luoyuy.top/posts/959140b987fa/
作者
LuoYu-Ying
发布于
2022年7月8日
许可协议