第3章:类、对象与接口(Classes, Objects and Interfaces)
一、导读
1.1 本章学习目标
第3章是 Kotlin 面向对象编程的核心。Kotlin 的类系统在设计上比 Java 更简洁、更安全,同时引入了许多强大的新特性。本章将深入讲解 Kotlin 的类、对象和接口,帮助你理解 Kotlin 如何用更少的代码实现更强大的面向对象设计。
本章的核心学习目标包括:
- 掌握 Kotlin 类的初始化机制:主构造函数、次构造函数、初始化块的协作方式
- 深入理解 `open`、`abstract`、`sealed` 类修饰符:Kotlin 默认类是 final 的,这改变了继承的设计思维
- 掌握可见性修饰符:
public、private、protected、internal的区别,特别是internal的独特作用 - 理解数据类(data class)和密封类(sealed class)的设计模式
- 掌握委托(delegation)模式的 Kotlin 原生支持
- 理解 `object` 关键字的多种用途:单例、伴生对象、对象表达式
1.2 为什么本章如此重要
Kotlin 的类系统做出了几个关键的设计决策,这些决策深刻影响了代码的质量和可维护性。
默认 final:Kotlin 的类默认是 final 的,这意味着你不能继承一个类,除非显式标记为 open。这个看似"限制"的设计实际上遵循了"组合优于继承"的原则,防止了不经意的继承导致的脆弱基类问题。
密封类:sealed class 是 Kotlin 独有的特性,它限制了类的继承层次,使得 when 表达式可以对所有子类进行穷举检查。这是实现代数数据类型(ADT)的关键工具。
委托模式:Kotlin 通过 by 关键字原生支持委托模式,让"组合优于继承"原则的实现变得极其简单。
1.3 核心问题引导
为什么 Kotlin 的类默认是 final 的?这个设计决策背后的考量是什么?
data class 自动生成了哪些方法?在什么情况下你需要自定义 equals 或 hashCode?
sealed class 和 enum class 有什么区别?各自适用于什么场景?
Kotlin 的 object 关键字有多少种用法?它们之间有什么联系?
委托模式(by 关键字)如何替代传统的继承?
二、核心概念详解
2.1 构造函数与初始化
2.1.1 主构造函数
Kotlin 的构造函数语法比 Java 简洁得多——主构造函数的参数直接声明在类头中:
class Person(val name: String, var age: Int) {
// 类体
init {
println("Person created: $name, age $age")
}
}
// 等价于 Java 中:
// class Person {
// private final String name;
// private int age;
// public Person(String name, int age) {
// this.name = name;
// this.age = age;
// System.out.println("Person created: " + name + ", age " + age);
// }
// // getter/setter...
// }如果主构造函数有注解或可见性修饰符,需要加 constructor 关键字:
class Person private constructor(val name: String) {
// 私有构造函数,只能通过伴生对象的工厂方法创建
companion object {
fun create(name: String): Person = Person(name)
}
}2.1.2 次构造函数
次构造函数使用 constructor 关键字声明,必须直接或间接委托给主构造函数:
class Person(val name: String, var age: Int) {
// 次构造函数
constructor(name: String) : this(name, 0)
// 委托给另一个次构造函数
constructor() : this("Unknown")
}次构造函数的一个常见用途是与 Java 框架的互操作(如 Spring 的多构造函数注入)。
2.1.3 初始化块
init 块中的代码在对象创建时执行,可以有多个 init 块:
class Config(val properties: Map<String, String>) {
val host: String
val port: Int
init {
host = properties["host"] ?: "localhost"
}
init {
port = properties["port"]?.toIntOrNull() ?: 8080
}
}2.2 继承与多态
2.2.1 open 修饰符
Kotlin 的类和方法默认是 final 的。要允许继承或覆盖,必须显式标记为 open:
open class Animal(val name: String) {
open fun sound(): String = "..."
fun breathe() = println("Breathing") // final,不能被覆盖
}
class Dog(name: String) : Animal(name) {
override fun sound(): String = "Woof!"
}
class Cat(name: String) : Animal(name) {
override fun sound(): String = "Meow!"
}这个设计决策的哲学是:继承应该是有意为之的设计决策,而不是偶然的代码复用。默认 final 迫使开发者明确思考一个类是否应该被继承。
2.2.2 抽象类
abstract class Shape {
abstract fun area(): Double
abstract fun perimeter(): Double
// 可以有具体实现
fun describe(): String = "Shape with area ${area()} and perimeter ${perimeter()}"
}
class Circle(val radius: Double) : Shape() {
override fun area(): Double = Math.PI * radius * radius
override fun perimeter(): Double = 2 * Math.PI * radius
}
class Rectangle(val width: Double, val height: Double) : Shape() {
override fun area(): Double = width * height
override fun perimeter(): Double = 2 * (width + height)
}2.2.3 密封类(Sealed Class)
密封类限制了类的继承层次——所有子类必须在同一个文件中定义:
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
object Loading : Result<Nothing>()
}
fun handleResult(result: Result<String>) = when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Error -> println("Error: ${result.exception.message}")
Result.Loading -> println("Loading...")
// 不需要 else 分支!编译器知道所有可能的子类
}密封类的关键优势是穷举检查——when 表达式可以覆盖所有可能的子类,编译器会检查你是否遗漏了某个分支。这使得密封类成为实现状态机、代数数据类型的理想选择。
2.3 可见性修饰符
Kotlin 有四种可见性修饰符:
| 修饰符 | 类成员 | 顶层声明 |
|---|---|---|
public(默认) | 任何地方可见 | 任何地方可见 |
internal | 同一模块可见 | 同一模块可见 |
protected | 该类及其子类可见 | 不适用 |
private | 同一类可见 | 同一文件可见 |
internal 是 Kotlin 独有的修饰符,它表示"模块内部可见"。模块可以是一个 Maven 项目、一个 Gradle 模块或一组一起编译的文件。internal 非常适合实现库的内部 API——对外部用户隐藏,但对库内部的代码可见。
// 在库的内部实现中
internal class DatabaseConnection(val url: String) {
internal fun connect() { /* ... */ }
internal fun disconnect() { /* ... */ }
}
// 公开的 API
class UserRepository {
private val connection = DatabaseConnection("jdbc:...")
fun findUser(id: Long): User? {
connection.connect()
// ...
connection.disconnect()
}
}2.4 数据类深入
2.4.1 数据类的规则
数据类必须满足以下要求:
- 主构造函数至少有一个参数
- 所有主构造函数参数必须标记为
val或var - 不能是
abstract、open、sealed或inner - 可以實現接口或继承抽象类(从 Kotlin 1.1 开始)
2.4.2 自动生成的方法
data class User(val name: String, val age: Int)
val user1 = User("Alice", 30)
val user2 = User("Alice", 30)
val user3 = User("Bob", 25)
// equals 和 hashCode
println(user1 == user2) // true(基于内容的比较)
println(user1 == user3) // false
// toString
println(user1) // User(name=Alice, age=30)
// copy
val olderUser = user1.copy(age = 31)
println(olderUser) // User(name=Alice, age=31)
// 解构声明
val (name, age) = user1
println("$name, $age years old")2.4.3 data class 与不可变性
数据类推荐与不可变属性(val)一起使用:
// 推荐:不可变数据类
data class Point(val x: Int, val y: Int)
// 通过 copy 创建修改后的副本
val p1 = Point(1, 2)
val p2 = p1.copy(x = 3) // Point(3, 2)2.5 object 关键字
object 关键字在 Kotlin 中有三种用法:
2.5.1 对象声明(单例)
object DatabaseConfig {
var url: String = ""
var maxConnections: Int = 10
fun init(url: String, maxConnections: Int) {
this.url = url
this.maxConnections = maxConnections
}
}
// 使用
DatabaseConfig.init("jdbc:mysql://localhost", 20)2.5.2 伴生对象
伴生对象与类关联,可以访问类的私有成员:
class User private constructor(val name: String) {
companion object {
private var count = 0
fun create(name: String): User {
count++
return User(name)
}
fun getCount(): Int = count
}
}
val user = User.create("Alice")
println(User.getCount()) // 12.5.3 对象表达式
对象表达式用于创建匿名对象,类似于 Java 的匿名内部类:
val listener = object : ActionListener {
override fun actionPerformed(e: ActionEvent) {
println("Button clicked")
}
}2.6 委托模式
Kotlin 通过 by 关键字原生支持委托模式:
interface Base {
fun print()
}
class BaseImpl(val x: Int) : Base {
override fun print() { println("BaseImpl: $x") }
}
// Derived 将 Base 接口的实现委托给 BaseImpl
class Derived(b: Base) : Base by b
fun main() {
val b = BaseImpl(10)
Derived(b).print() // 调用 BaseImpl.print()
}委托模式的一个实际应用是实现装饰器:
interface Cache {
fun get(key: String): String?
fun put(key: String, value: String)
}
class LoggingCache(private val cache: Cache) : Cache by cache {
override fun get(key: String): String? {
println("Cache GET: $key")
return cache.get(key)
}
override fun put(key: String, value: String) {
println("Cache PUT: $key = $value")
cache.put(key, value)
}
}三、实战案例
3.1 案例一:使用密封类实现表达式求值器
sealed class Expr {
data class Num(val value: Double) : Expr()
data class BinOp(val left: Expr, val op: String, val right: Expr) : Expr()
data class UnaryOp(val op: String, val operand: Expr) : Expr()
}
fun evaluate(expr: Expr): Double = when (expr) {
is Expr.Num -> expr.value
is Expr.BinOp -> {
val left = evaluate(expr.left)
val right = evaluate(expr.right)
when (expr.op) {
"+" -> left + right
"-" -> left - right
"*" -> left * right
"/" -> left / right
else -> throw IllegalArgumentException("Unknown operator: ${expr.op}")
}
}
is Expr.UnaryOp -> {
val operand = evaluate(expr.operand)
when (expr.op) {
"-" -> -operand
else -> throw IllegalArgumentException("Unknown operator: ${expr.op}")
}
}
}
fun prettyPrint(expr: Expr, indent: Int = 0): String {
val prefix = " ".repeat(indent)
return when (expr) {
is Expr.Num -> "$prefix${expr.value}"
is Expr.BinOp -> "$prefix(${expr.op}\n${prettyPrint(expr.left, indent + 2)}\n${prettyPrint(expr.right, indent + 2)}\n$prefix)"
is Expr.UnaryOp -> "$prefix(${expr.op}\n${prettyPrint(expr.operand, indent + 2)}\n$prefix)"
}
}
fun main() {
// (3 + 4) * 2
val expr = Expr.BinOp(
Expr.BinOp(Expr.Num(3.0), "+", Expr.Num(4.0)),
"*",
Expr.Num(2.0)
)
println("Expression: ${prettyPrint(expr)}")
println("Result: ${evaluate(expr)}") // 14.0
}3.2 案例二:委托属性实现懒加载
class ExpensiveResource {
init { println("ExpensiveResource created!") }
fun use() = println("Using resource")
}
class Application {
// 懒加载:第一次访问时才创建
val resource by lazy {
println("Initializing resource...")
ExpensiveResource()
}
fun doWork() {
println("Starting work...")
resource.use()
}
}
fun main() {
val app = Application()
println("App created")
app.doWork()
app.doWork() // 第二次访问不会重新创建
}
// 输出:
// App created
// Starting work...
// Initializing resource...
// ExpensiveResource created!
// Using resource
// Using resource3.3 案例三:使用伴生对象实现工厂模式
sealed class PaymentMethod {
data class CreditCard(val number: String, val expiry: String) : PaymentMethod()
data class BankTransfer(val accountNumber: String, val bankCode: String) : PaymentMethod()
data class DigitalWallet(val provider: String, val email: String) : PaymentMethod()
companion object {
fun fromJson(json: Map<String, String>): PaymentMethod {
return when (json["type"]) {
"credit_card" -> CreditCard(
json["number"] ?: error("Missing card number"),
json["expiry"] ?: error("Missing expiry")
)
"bank_transfer" -> BankTransfer(
json["account"] ?: error("Missing account"),
json["bank_code"] ?: error("Missing bank code")
)
"digital_wallet" -> DigitalWallet(
json["provider"] ?: error("Missing provider"),
json["email"] ?: error("Missing email")
)
else -> throw IllegalArgumentException("Unknown payment type: ${json["type"]}")
}
}
}
}四、本章小结
第3章深入讲解了 Kotlin 的类系统:
构造函数:主构造函数参数直接声明在类头中,结合 init 块实现灵活的初始化逻辑。
默认 final:Kotlin 类默认不可继承,迫使开发者有意识地设计继承关系。
密封类:限制继承层次,支持 when 的穷举检查,是实现代数数据类型的利器。
数据类:自动生成 equals、hashCode、toString、copy 和解构方法。
object 关键字:单例、伴生对象、对象表达式三种用法。
委托模式:by 关键字让委托模式的实现变得极其简洁。
五、思考与练习
思考题
Kotlin 默认类是 final 的设计决策,对大型项目的代码质量有什么影响?
data class 的 copy() 方法在什么场景下特别有用?它与不可变性有什么关系?
密封类和枚举类都能实现"有限集合",它们的适用场景有什么不同?
委托模式(by)和继承相比有什么优势?在什么场景下应该优先使用委托?
编程练习
创建一个密封类 HttpResult,包含 Success(带泛型数据)、Error(带状态码和消息)、Redirect(带目标 URL)三个子类。编写一个函数处理所有可能的结果。
实现一个 ImmutableStack<T> 数据类,使用 copy() 方法实现 push 和 pop 操作,返回新的栈实例。
使用委托模式实现一个 CachedRepository,它装饰了一个 Repository 接口,为 findById 方法添加缓存功能。