3Kotlin in Action

类、对象与接口

面向对象

数据类密封类接口默认方法委托

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 简洁得多——主构造函数的参数直接声明在类头中:

kotlin
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 关键字:

kotlin
class Person private constructor(val name: String) {
    // 私有构造函数,只能通过伴生对象的工厂方法创建
    companion object {
        fun create(name: String): Person = Person(name)
    }
}

2.1.2 次构造函数

次构造函数使用 constructor 关键字声明,必须直接或间接委托给主构造函数:

kotlin
class Person(val name: String, var age: Int) {
    // 次构造函数
    constructor(name: String) : this(name, 0)
    
    // 委托给另一个次构造函数
    constructor() : this("Unknown")
}

次构造函数的一个常见用途是与 Java 框架的互操作(如 Spring 的多构造函数注入)。

2.1.3 初始化块

init 块中的代码在对象创建时执行,可以有多个 init 块:

kotlin
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:

kotlin
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 抽象类

kotlin
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)

密封类限制了类的继承层次——所有子类必须在同一个文件中定义:

kotlin
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——对外部用户隐藏,但对库内部的代码可见。

kotlin
// 在库的内部实现中
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 自动生成的方法

kotlin
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)一起使用:

kotlin
// 推荐:不可变数据类
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 对象声明(单例)

kotlin
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 伴生对象

伴生对象与类关联,可以访问类的私有成员:

kotlin
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())    // 1

2.5.3 对象表达式

对象表达式用于创建匿名对象,类似于 Java 的匿名内部类:

kotlin
val listener = object : ActionListener {
    override fun actionPerformed(e: ActionEvent) {
        println("Button clicked")
    }
}

2.6 委托模式

Kotlin 通过 by 关键字原生支持委托模式:

kotlin
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()
}

委托模式的一个实际应用是实现装饰器:

kotlin
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 案例一:使用密封类实现表达式求值器

kotlin
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 案例二:委托属性实现懒加载

kotlin
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 resource

3.3 案例三:使用伴生对象实现工厂模式

kotlin
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 方法添加缓存功能。


3/8类、对象
0%