5.8.1 类

原文链接: https://kotlinlang.org/docs/classes.html

5.8.1 类

提示: 在创建类之前,如果目的是保存数据,请考虑使用数据类。或者考虑用扩展来扩展已有类,而不是从零创建一个新类。

与其他面向对象语言一样,Kotlin 使用类来封装数据(属性)和行为(函数),以编写可复用、结构化的代码。

类是对象的蓝图或模板,通过构造函数来创建对象。当你创建类的实例时,就是在根据该蓝图创建一个具体的对象。

Kotlin 提供了简洁的类声明语法。要声明一个类,请使用 class 关键字,后面跟类名:

1
class Person { /*...*/ }

类声明由以下部分组成:

你可以把类头部和类体都保持到最简。如果类没有类体,可以省略花括号 {}:

1
2
// 带主构造函数但没有类体的类
class Person(val name: String, var age: Int)

下面这个例子声明了一个带头部和类体的类,然后从中创建实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Person 类及其主构造函数,
// 它初始化 name 属性
class Person(val name: String) {
    // 带 age 属性的类体
    var age: Int = 0
}

fun main() {
    // 通过调用构造函数创建 Person 类的实例
    val person = Person("Alice")

    // 访问实例的属性
    println(person.name)
    // Alice
    println(person.age)
    // 0
}

创建实例

实例是这样创建的:你以类为蓝图构建出一个真实对象,供程序使用。

要创建类的实例,请使用类名后跟圆括号 (),类似于调用函数:

1
2
// 创建 Person 类的实例
val anonymousUser = Person()

在 Kotlin 中,你可以这样创建实例:

  • 不带实参(Person()):如果类中声明了默认值,则使用默认值创建实例。
  • 带实参(Person(value)):通过传入具体值来创建实例。

你可以把创建的实例赋给可变(var)或只读(val)变量:

1
2
3
4
5
6
7
// 使用默认值创建实例
// 并把它赋给可变变量
var anonymousUser = Person()

// 传入具体值创建实例
// 并把它赋给只读变量
val namedUser = Person("Joe")

你可以在任何需要的地方创建实例,例如 main() 函数中、其他函数中,或另一个类中。此外,你也可以在另一个函数中创建实例,并从 main() 调用该函数。

下面的代码声明了一个带名称属性的 Person 类。它还演示了如何用默认构造函数的值和具体值创建实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 带主构造函数的类头部,
// 它用默认值初始化 name
class Person(val name: String = "Sebastian")

fun main() {
    // 使用默认构造函数的值创建实例
    val anonymousUser = Person()

    // 传入具体值创建实例
    val namedUser = Person("Joe")

    // 访问实例的 name 属性
    println(anonymousUser.name)
    // Sebastian
    println(namedUser.name)
    // Joe
}

注意: 与其他面向对象编程语言不同,Kotlin 创建类实例时不需要 new 关键字。

关于创建嵌套类、内部类和匿名内部类实例的信息,请参阅嵌套类一节。

构造函数与初始化块

创建类实例时,你会调用它的某个构造函数。Kotlin 中的类可以有一个主构造函数和一个或多个次构造函数。

主构造函数是初始化类的主要方式,在类头部声明。次构造函数提供额外的初始化逻辑,在类体中声明。

主构造函数和次构造函数都是可选的,但一个类必须至少有一个构造函数。

主构造函数

主构造函数在实例创建时设置实例的初始状态。

要声明主构造函数,请把它写在类头部、类名之后:

1
class Person constructor(name: String) { /*...*/ }

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

1
class Person(name: String) { /*...*/ }

主构造函数可以把参数声明为属性。在参数名之前使用 val 关键字声明只读属性,使用 var 关键字声明可变属性:

1
class Person(val name: String, var age: Int) { /*...*/ }

这些构造函数参数属性会作为实例的一部分保存,并且可以从类外部访问。

也可以声明不是属性的主构造函数参数。这些参数前面没有 val 或 var,因此不会保存在实例中,只在类体内部可用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 既是属性又是主构造函数参数
class PersonWithProperty(val name: String) {
    fun greet() {
        println("Hello, $name")
    }
}

// 仅作为主构造函数参数(不保存为属性)
class PersonWithAssignment(name: String) {
    // 必须赋给属性才能之后使用
    val displayName: String = name

    fun greet() {
        println("Hello, $displayName")
    }
}

主构造函数中声明的属性可以被类的成员函数访问:

1
2
3
4
5
6
7
// 声明属性的主构造函数所在的类
class Person(val name: String, var age: Int) {
    // 访问类属性的成员函数
    fun introduce(): String {
        return "Hi, I'm $name and I'm $age years old."
    }
}

你也可以为主构造函数中的属性指定默认值:

1
class Person(val name: String = "John", var age: Int = 30) { /*...*/ }

如果在创建实例时没有向构造函数传值,属性就会使用其默认值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 带主构造函数的类,
// 其中 name 和 age 都有默认值
class Person(val name: String = "John", var age: Int = 30)

fun main() {
    // 使用默认值创建实例
    val person = Person()
    println("Name: ${person.name}, Age: ${person.age}")
    // Name: John, Age: 30
}

你可以在类体中直接用主构造函数参数初始化额外的类属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 带主构造函数的类,
// 其中 name 和 age 都有默认值
class Person(
    val name: String = "John",
    var age: Int = 30
) {
    // 根据主构造函数参数
    // 初始化 description 属性
    val description: String = "Name: $name, Age: $age"
}

fun main() {
    // 创建 Person 类的实例
    val person = Person()
    // 访问 description 属性
    println(person.description)
    // Name: John, Age: 30
}

与函数一样,构造函数声明中也可以使用尾随逗号:

1
2
3
4
5
class Person(
    val name: String,
    val lastName: String,
    var age: Int,
) { /*...*/ }

初始化块

主构造函数负责初始化类并设置其属性。大多数情况下,用简单的代码就能完成。

如果创建实例时需要执行更复杂的操作,请把该逻辑放在类体中的初始化块里。这些块会在主构造函数执行时运行。

用 init 关键字后跟花括号 {} 声明初始化块。在花括号中写下你希望在初始化期间运行的任何代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 带主构造函数的类,其初始化 name 和 age
class Person(val name: String, var age: Int) {
    init {
        // 创建实例时运行初始化块
        println("Person created: $name, age $age.")
    }
}

fun main() {
    // 创建 Person 类的实例
    Person("John", 30)
    // Person created: John, age 30.
}

你可以按需要添加任意多个初始化块(init {})。它们与属性初始化器一起,按在类体中出现的顺序运行:

 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
// 带主构造函数的类,其初始化 name 和 age
class Person(val name: String, var age: Int) {
    // 第一个初始化块
    init {
        // 创建实例时最先运行
        println("Person created: $name, age $age.")
    }

    // 第二个初始化块
    init {
        // 在第一个初始化块之后运行
        if (age < 18) {
            println("$name is a minor.")
        } else {
            println("$name is an adult.")
        }
    }
}

fun main() {
    // 创建 Person 类的实例
    Person("John", 30)
    // Person created: John, age 30.
    // John is an adult.
}

你可以在初始化块中使用主构造函数参数。例如在上面的代码中,第一个和第二个初始化器都使用了主构造函数的 name 和 age 参数。

init 块的一个常见用途是数据校验。例如调用 require 函数:

1
2
3
4
5
class Person(val age: Int) {
    init {
        require(age > 0) { "age must be positive" }
    }
}

次构造函数

在 Kotlin 中,次构造函数是类在主构造函数之外可以拥有的额外构造函数。当你需要多种初始化类的方式,或需要 Java 互操作时,次构造函数很有用。

要声明次构造函数,请在类体中使用 constructor 关键字,并把构造函数参数放在圆括号 () 中。把构造函数逻辑写在花括号 {} 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 带主构造函数的类头部,其初始化 name 和 age
class Person(val name: String, var age: Int) {

    // 次构造函数,把 age 作为
    // String 接收并转换为 Int
    constructor(name: String, age: String) : this(name, age.toIntOrNull() ?: 0) {
        println("$name created with converted age: ${this.age}")
    }
}

fun main() {
    // 使用把 age 作为 String 的次构造函数
    Person("Bob", "8")
    // Bob created with converted age: 8
}

提示: 表达式 age.toIntOrNull() ?: 0 使用了 Elvis 运算符。更多信息请参阅空安全。

在上面的代码中,次构造函数通过 this 关键字委托给主构造函数,传入 name 以及转换后的整数 age 值。

在 Kotlin 中,次构造函数必须委托给主构造函数。这种委托确保所有主构造函数初始化逻辑都会在任何次构造函数逻辑运行之前执行。

构造函数委托可以是:

  • 直接的,即次构造函数直接调用主构造函数。
  • 间接的,即一个次构造函数调用另一个次构造函数,后者再委托给主构造函数。

下面这个例子演示了直接委托和间接委托的工作方式:

 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
// 带主构造函数的类头部,其初始化 name 和 age
class Person(
    val name: String,
    var age: Int
) {
    // 直接委托给
    // 主构造函数的次构造函数
    constructor(name: String) : this(name, 0) {
        println("Person created with default age: $age and name: $name.")
    }

    // 间接委托的次构造函数:
    // this("Bob") -> constructor(name: String) -> 主构造函数
    constructor() : this("Bob") {
        println("New person created with default age: $age and name: $name.")
    }
}

fun main() {
    // 基于直接委托创建实例
    Person("Alice")
    // Person created with default age: 0 and name: Alice.

    // 基于间接委托创建实例
    Person()
    // Person created with default age: 0 and name: Bob.
    // New person created with default age: 0 and name: Bob.
}

在带初始化块(init {})的类中,这些块中的代码会成为主构造函数的一部分。由于次构造函数会先委托给主构造函数,所以所有初始化块和属性初始化器都会在次构造函数的主体之前运行。即使类没有主构造函数,这种委托仍会隐式发生:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 没有主构造函数的类头部
class Person {
    // 创建实例时运行初始化块
    init {
        // 在次构造函数之前运行
        println("1. First initializer block runs")
    }

    // 接收整数参数的次构造函数
    constructor(i: Int) {
        // 在初始化块之后运行
        println("2. Person $i is created")
    }
}

fun main() {
    // 创建 Person 类的实例
    Person(1)
    // 1. First initializer block runs
    // 2. Person 1 created
}

没有构造函数的类

没有声明任何构造函数(主构造函数或次构造函数)的类,会有一个隐式的无参主构造函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 没有显式构造函数的类
class Person {
    // 没有声明主构造函数或次构造函数
}

fun main() {
    // 使用隐式主构造函数
    // 创建 Person 类的实例
    val person = Person()
}

这个隐式主构造函数的可见性是 public,也就是说可以从任何地方访问。如果你不希望类有公开构造函数,请声明一个带非默认可见性的空主构造函数:

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

注意: 在 JVM 上,如果主构造函数的所有参数都有默认值,编译器会隐式提供一个使用这些默认值的无参构造函数。这让 Kotlin 更容易与 Jackson 或 Spring Data JPA 这类通过无参构造函数创建类实例的库配合使用。在下面的例子中,Kotlin 会隐式提供使用默认值 "" 的无参构造函数 Person():kotlin class Person(val personName: String = "")

继承

Kotlin 中的类继承允许你从已有类(基类)创建新类(派生类),继承其属性和函数,同时添加或修改行为。

关于继承层级以及如何使用 open 关键字的详细信息,请参阅继承一节。

抽象类

在 Kotlin 中,抽象类是不能被直接实例化的类。它们被设计为由其他类继承,由后者定义其实际行为。这种行为称为实现。

抽象类可以声明抽象属性和函数,这些必须由子类实现。

抽象类也可以有构造函数。这些构造函数用于初始化类属性,并强制子类提供必要参数。使用 abstract 关键字声明抽象类:

1
abstract class Person(val name: String, val age: Int)

抽象类可以同时有抽象成员和非抽象成员(属性和函数)。要把成员声明为抽象,必须显式使用 abstract 关键字。

你不需要用 open 关键字标注抽象类或抽象函数,因为它们默认就是可以被继承的。关于 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
28
29
30
31
32
33
34
35
36
37
38
39
// 带主构造函数的抽象类,其声明 name 和 age
abstract class Person(
    val name: String,
    val age: Int
) {
    // 抽象成员
    // 不提供实现,
    // 必须由子类实现
    abstract fun introduce()

    // 非抽象成员(有实现)
    fun greet() {
        println("Hello, my name is $name.")
    }
}

// 为抽象成员提供实现的子类
class Student(
    name: String,
    age: Int,
    val school: String
) : Person(name, age) {
    override fun introduce() {
        println("I am $name, $age years old, and I study at $school.")
    }
}

fun main() {
    // 创建 Student 类的实例
    val student = Student("Alice", 20, "Engineering University")

    // 调用非抽象成员
    student.greet()
    // Hello, my name is Alice.

    // 调用被重写的抽象成员
    student.introduce()
    // I am Alice, 20 years old, and I study at Engineering University.
}

伴生对象

在 Kotlin 中,每个类都可以有一个伴生对象。伴生对象是一种对象声明,让你无需创建类实例就能用类名访问其成员。

假设你需要编写一个无需创建类实例就能调用的函数,但它仍然在逻辑上与类相关(例如工厂函数)。这种情况下,可以在类内部的伴生对象声明中声明它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 带主构造函数的类,其声明 name 属性
class Person(
    val name: String
) {
    // 带伴生对象的类体
    companion object {
        fun createAnonymous() = Person("Anonymous")
    }
}

fun main() {
    // 无需创建类实例即可调用该函数
    val anonymous = Person.createAnonymous()
    println(anonymous.name)
    // Anonymous
}

如果在类中声明了伴生对象,就可以只用类名作为限定符来访问其成员。

更多信息请参阅伴生对象。