08. 定义类

最简单的类,因为该类没有主体所以可省略花括号。

1
class Customer

声明一个名为 Customer 的类,不带任何属性或用户定义的构造函数。kotlin 会自动创建一个非参数化的默认构造函数。
通过默认构造函数创建 Customer 类的实例。请注意,kotlin 中没有新的关键字。

声明具有两个属性的类

1
2
3
4
5
6
7
class Contact(val id: Int, var email: String) // 1

fun main() {
val contact = Contact(1, "zhang@qq.com") // 2
println(contact.id) // 3
contact.email = "jane@gmail.com" // 4
}
  1. 声明具有两个属性的类:不可变的 id 和可变的 email 以及具有两个参数 id 和 email 的构造函数。
  2. 使用具有两个参数的构造函数创建 Contact 类的实例。
  3. 访问属性 ID。
  4. 更新属性 email 的值。

构造函数

kotlin 的类可以有一个主构造函数和一个或多个次构造函数。主构造函数是类头的一部分,它位于类名和可选类型参数之后。

我们当然可以写出和 Java 很相似的基本写法:

1
2
3
4
5
6
class User {
var name: String
constructor(name: String) {
this.name = name
}
}

主构造函数

1
class User(val userName: String = "")

注意:次构造函数不能用来像主构造函数那样定义属性。类属性必须定义在主构造函数里,或者至少要定义在类层级。

但是 Kotlin 中还有更简单的方法来写构造器:

1
2
3
4
5
               👇
class User constructor(name: String) {
// 👇 这里与构造器中的 name 是同一个
var name: String = name
}

这个写法叫「主构造器 primary constructor」。与之相对的在第二篇中,写在类中的构造器被称为「次构造器」。在 Kotlin 中一个类最多只能有 1 个主构造器(也可以没有),而次构造器是没有个数限制。

1
2
3
4
5
6
class User constructor(name: String) {
var name: String
init {
this.name = name
}
}

注意主构造函数不能包含任何代码。初始化代码可以放在初始化程序块前缀为 init 关键字。init 代码块是紧跟在主构造器之后执行的,这是因为主构造器本身没有代码体,init 代码块就充当了主构造器代码体的功能。

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

1
2
3
class User(name: String) {
var name: String = name
}

如果构造函数使用了注解或可见性修饰符,则构造器关键字是必需的,修饰符位于其前面:

1
2
// 加了注解
class User public @Inject constructor(name: String) { /*...*/ }
1
2
// 加了可见性修饰符
class User private constructor(name: String) { /*...*/ }

主构造函数参数可以在初始化程序块中使用。它们也可以用在类体中声明的属性初始化式中:

1
2
3
class User(name: String) {
val customerKey = name.uppercase()
}

kotlin 有一个简洁的语法来声明属性并从主构造函数初始化它们:

1
class User(val firstName: String, val lastName: String, var age: Int)

此类声明还可以包括类属性的默认值:

1
class User(val firstName: String, val lastName: String, var isEmployed: Boolean = true)

声明类属性时,可以使用尾随逗号:

1
2
3
4
5
class User(
val firstName: String,
val lastName: String,
var age: Int, // trailing comma
) { /*...*/ }

与常规属性非常相似,在主构造函数中声明的属性可以是可变的(var)或只读的(val)。

注意:如果一个非抽象类没有声明任何构造函数(主构造函数或次构造函数),它将生成一个没有参数的主构造函数。构造函数的可见性将是公共的。

如果你不想让你的类有一个公共构造函数,那么就声明一个非默认可见的空主构造函数:

1
class DontCreateMe private constructor() { /*...*/ }

在 JVM 中,如果所有的主构造函数参数都有默认值,编译器将生成一个额外的无参数构造函数,它将使用默认值。这使得 kotlin 更容易与 Jackson 或 JPA 等库一起使用,这些库通过无参数构造函数创建类实例。

次构造函数

类还可以声明次要构造函数,它们的前缀为 constructor:

1
2
3
4
5
6
7
class User(val pets: MutableList<Pet> = mutableListOf())

class Pet {
constructor(owner: User) {
owner.pets.add(this) // adds this pet to the list of its owner's pets
}
}

如果类具有主构造函数,则每个辅助构造函数都需要委托给主构造函数,可以直接委托,也可以通过其他辅助构造函数间接委托。对同一类的另一个构造函数的委托是使用这个关键词:

1
2
3
4
5
6
class User(val name: String) {
val children: MutableList<User> = mutableListOf()
constructor(name: String, parent: User) : this(name) {
parent.children.add(this)
}
}

初始值设定项块中的代码实际上成为主构造函数的一部分。对主构造函数的委托发生在辅助构造函数的第一条语句中,因此所有初始值设定块和属性初始值设定项中的代码都在辅助构造函数体之前执行。

即使类没有主构造函数,委托仍然隐式发生,初始化块仍然执行:

1
2
3
4
5
6
7
8
9
class Constructors {
init {
println("Init block")
}

constructor(i: Int) {
println("Constructor $i")
}
}

创建实例

若要创建类的实例,请像调用常规函数一样调用构造函数:

1
2
val invoice = Invoice()
val customer = Customer("Joe Smith")

类的初始化顺序总结

(1) 主构造函数里声明的属性。
(2) 类级别的属性赋值
(3) init 初始化块里的属性赋值和函数调用
(4) 次构造函数里的属性赋值和函数调用

需要说明的是,init 初始化块(线条 3)和类级别的属性赋值(线条 2)的顺序取决于定义 的先后。如果 init 初始化块定义在类级别的属性赋值之前,那么它就比类级别的属性赋值早一步初始化。但是强烈建议按照上述顺序定义类。

继承

Kotlin 完全支持传统的面向对象继承机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
open class Dog {                // 1
open fun sayHello() { // 2
println("wow wow!")
}
}

class Yorkshire : Dog() { // 3
override fun sayHello() { // 4
println("wif wif!")
}
}

fun main() {
val dog: Dog = Yorkshire()
dog.sayHello()
}
  1. 默认情况下,Kotlin 类是 final 类。如果要允许类继承,请使用 open 修饰符标记该类。
  2. Kotlin 方法在默认情况下也是 final 方法。
  3. 当在类名后面指定 : SuperclassName() 时,类继承超类。空括号()表示对超类缺省构造函数的调用。
  4. 重写方法或属性需要 override 修饰符。

参数化构造函数的继承

1
2
3
4
5
6
7
8
9
10
11
12
open class Tiger(val origin: String) {
fun sayHello() {
println("A tiger from $origin says: grrhhh!")
}
}

class SiberianTiger : Tiger("Siberia")

fun main() {
val tiger: Tiger = SiberianTiger()
tiger.sayHello()
}

如果您想在创建子类时使用超类的参数化构造函数,请在子类声明中提供构造函数参数。

将构造函数参数传递给超类

1
2
3
4
5
6
7
8
9
10
11
12
open class Lion(val name: String, val origin: String) {
fun sayHello() {
println("$name, the lion from $origin says: graoh!")
}
}

class Asiatic(name: String) : Lion(name = name, origin = "CN")

fun main() {
val lion: Lion = Asiatic("Rufo")
lion.sayHello()
}
  1. 注意 Asiatic 声明中的 name 既不是 var 也不是 val: 它是一个构造函数参数,其值被传递给超类 Lion 的 name 属性。
  2. 创建一个名为 Rufo 的 Asiatic 实例。

总结

参考