第4章:面向对象编程

类、继承、接口、数据类、密封类 —— 以运筹帷幄之道,布局 Kotlin OOP 的战略蓝图

🪶

本章导师:诸葛亮

核心方法论:运筹帷幄,先设计蓝图再排兵布阵

「兵者,国之大事也。编程亦然。面向对象之道,犹如排兵布阵——类是你的兵种,继承是指挥链,接口是军令协议,密封类是独门阵法。不懂布局者,代码必乱如散兵;善于谋划者,架构自成铁壁。今日,吾将以军略之道,带各位将军领悟 Kotlin OOP 的精髓。切记:先画蓝图,再动代码。未战而庙算者胜!」

4.1 类与构造器 —— 定义你的兵种

基本类定义

三军未动,兵种先行。在 Kotlin 的战场上,就是你麾下的兵种蓝图。定义一个兵种可以极其简洁——构造器参数直接写在类名后面,加上 valvar 就自动成为该兵种的属性,无需冗余的样板代码:

class Person(val name: String, var age: Int)

// 使用
val person = Person("张三", 25)
println(person.name)  // 张三
person.age = 26       // var 可以修改
// person.name = "李四"  // 编译错误! val 不可修改

仅此一行,Kotlin 便为你铸造了一个包含两个属性、一个构造器的完整兵种蓝图。正所谓化繁为简,上兵伐谋。

主构造器 vs 次构造器

类名后面跟的便是主构造器(primary constructor),犹如兵种的主要招募方式。若需要额外的招募途径,可以使用 constructor 关键字定义次构造器(secondary constructor),作为后备方案:

class Person(val name: String, var age: Int) {

    var email: String = ""

    // 次构造器,必须委托给主构造器
    constructor(name: String, age: Int, email: String) : this(name, age) {
        this.email = email
    }
}

val p1 = Person("张三", 25)
val p2 = Person("李四", 30, "lisi@example.com")

军师锦囊

善用兵者,不以数量取胜。实际开发中,Kotlin 更推荐使用默认参数值来代替多个次构造器,化繁为简:
class Person(val name: String, var age: Int, var email: String = "")

init 块:练兵初始化

兵种招募之后,还需要检阅与操练。init 块就是你的"练兵场"——在对象创建时执行初始化逻辑,确保每一个新兵都合格:

class Person(val name: String, var age: Int) {

    init {
        require(age >= 0) { "年龄不能为负数" }
        println("创建了一个 Person: $name, $age 岁")
    }

    // 可以有多个 init 块,按顺序执行
    init {
        println("第二个 init 块")
    }
}

兵种蓝图对比:Kotlin vs Java vs C++

让我们以三国之势,看看同样的兵种蓝图在不同阵营中需要多少笔墨:

// Java — 一个简单的 Person 类需要这么多样板代码
public class Person {
    private final String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

// Kotlin — 一行搞定
class Person(val name: String, var age: Int)
C++ 开发者对照

C++ 的类默认可以被继承(除非标记为 final),而 Kotlin 的类默认就是 final 的,必须加 open 关键字才允许继承。这正如兵法所云:「善守者,藏于九地之下」——Kotlin 选择了默认封闭的防御策略,只在你明确需要时才开放继承。C++ 则是默认敞开城门,需要你自己加 final 去守卫。此外,Kotlin 的主构造器语法将参数声明与属性定义合二为一,省去了 C++ 中构造函数初始化列表和成员变量声明的繁琐。

4.2 属性 —— 兵种的装备与属性

属性:比字段更高明的布局

在排兵布阵中,每个兵种都有其核心属性——攻击力、防御力、速度。Kotlin 没有传统意义上的"字段"(field),你声明的是属性(property),编译器会自动为其配备 getter(对于 val)和 getter/setter(对于 var)。这就像为每个兵种自动配备了标准装备,无需手动打造:

class Rectangle(val width: Double, val height: Double) {
    // 只读属性,自动有 getter
    val area: Double
        get() = width * height
}

val rect = Rectangle(5.0, 3.0)
println(rect.area)  // 15.0 — 看起来像访问字段,实际调用了 getter

自定义 getter 和 setter

有时你需要为兵种的装备加入特殊效果。自定义 getter 和 setter 就是你的"附魔工坊"——在读写属性时注入自定义逻辑:

class User(name: String) {

    var name: String = name
        set(value) {
            // 自定义 setter: 自动去除首尾空格
            field = value.trim()
        }

    val nameLength: Int
        get() = name.length  // 自定义 getter: 每次访问时计算
}

val user = User("  Kotlin  ")
println(user.name)        // Kotlin(已去除空格)
println(user.nameLength)  // 6

幕后字段 field

在自定义 setter 中,field 是所谓的幕后字段(backing field),犹如军中密令,代表属性实际存储的值。切不可混淆——用属性名赋值会陷入"无限递归"的圈套:

var counter: Int = 0
    set(value) {
        if (value >= 0) {
            field = value  // field 指向真正存储值的地方
        }
        // 如果写 this.counter = value 会无限递归调用 setter!
    }

军令警告

在自定义 setter 中,一定要用 field 而不是属性名来赋值,否则会导致无限递归,最终 StackOverflowError。此乃兵家大忌,如同"追击敌军却绕回自家营寨",死循环矣!

延迟初始化:lateinit var

兵马未动,粮草先行——但有些"装备"无法在出征前备齐。比如 Android 中 onCreate 之后才能拿到 View,此时可以用 lateinit 先声明,稍后再装备:

class MyActivity : AppCompatActivity() {

    // 告诉编译器: 我保证在使用之前会初始化
    lateinit var textView: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        textView = findViewById(R.id.myTextView)  // 真正初始化
    }

    fun updateText() {
        // 可以检查是否已初始化
        if (this::textView.isInitialized) {
            textView.text = "Hello Kotlin"
        }
    }
}

lateinit 军规

惰性初始化:by lazy

by lazy 乃"以逸待劳"之策——用于 val 属性,只有在第一次访问时才会执行初始化逻辑,而且是线程安全的。不需要的装备绝不提前打造,节省资源:

class DatabaseHelper {

    // 只有在第一次访问 connection 时才会创建连接
    val connection: Connection by lazy {
        println("正在建立数据库连接...")
        DriverManager.getConnection("jdbc:mysql://localhost/mydb")
    }
}

val helper = DatabaseHelper()
println("DatabaseHelper 已创建")  // 此时还没有连接数据库
helper.connection                      // 第一次访问,触发 lazy 初始化
helper.connection                      // 第二次访问,直接返回缓存值
对比项 lateinit var by lazy
适用对象 var(可变属性) val(只读属性)
初始化时机 手动赋值,任何时候 第一次访问时自动执行
线程安全 不保证 默认线程安全
基本类型 不支持 支持
典型场景 Android View 绑定 昂贵资源的延迟加载

4.3 继承与接口 —— 指挥链与军令协议

Kotlin 类默认 final:铁壁防御

兵法云:「善守者,敌不知其所攻。」与 Java 不同,Kotlin 的类默认是 final 的——城门紧闭,不允许被继承。你必须显式地用 open 修饰符来开放继承,就像主动打开城门,欢迎盟军入驻:

// 默认不能被继承
class Animal   // 相当于 Java 的 final class Animal

// 加上 open 才能被继承
open class Animal(val name: String) {
    open fun makeSound() {
        println("...")
    }
}

为何默认铁壁?

此乃 Kotlin 的战略布局:"Effective Java" 第19条建议"要么为继承设计并提供文档,要么禁止继承"。Kotlin 在语言层面贯彻了这一防御思想,避免了"脆弱基类"问题——正如一座城池若不加防护便允许任何人进出,迟早被敌军渗透。

继承语法:建立指挥链

继承就是军队的指挥链——子类服从父类的蓝图,但可以因地制宜地重写战术。Kotlin 使用 :(冒号)代替 Java 的 extends,简洁而有力:

open class Person(val name: String, var age: Int) {
    open fun introduce() {
        println("我叫$name,今年$age岁")
    }
}

class Student(
    name: String,
    age: Int,
    val school: String
) : Person(name, age) {

    // 重写方法必须加 override
    override fun introduce() {
        super.introduce()
        println("我在$school上学")
    }
}

val student = Student("小明", 18, "清华大学")
student.introduce()
// 输出:
// 我叫小明,今年18岁
// 我在清华大学上学

接口:军令协议

接口就是军中协议——定义了各兵种必须遵守的行为规范。一个兵种可以同时遵循多份协议,而协议本身还可以提供默认实现,作为"通用战术手册":

interface Clickable {
    fun onClick()           // 抽象方法
    fun showRipple() {      // 有默认实现
        println("显示水波纹效果")
    }
}

interface Focusable {
    fun onFocus()
    fun showRipple() {      // 也有 showRipple 的默认实现
        println("显示焦点高亮")
    }
}

// 一个类可以实现多个接口
class Button : Clickable, Focusable {
    override fun onClick() {
        println("按钮被点击")
    }

    override fun onFocus() {
        println("按钮获得焦点")
    }

    // 两个接口都有 showRipple,必须手动指定
    override fun showRipple() {
        super<Clickable>.showRipple()  // 调用 Clickable 的默认实现
        super<Focusable>.showRipple()  // 也可以调用 Focusable 的
    }
}
C++ 开发者对照

C++ 支持多重继承,但由此带来了臭名昭著的菱形继承问题(Diamond Problem),需要用虚继承(virtual)来解决,布局复杂且容易出错。Kotlin 从根源上避免了这个问题——只允许单继承类,但可以实现多个接口。接口可以有默认实现(类似 C++ 的纯虚函数 + 默认实现),当多个接口存在同名方法时,编译器会强制你用 super<InterfaceName> 明确指定调用哪个,绝不留下歧义。此乃"以规矩定方圆"之道。

抽象类:大将之蓝图

抽象类是不可直接实例化的"高级兵种蓝图"——它定义了框架,具体实现交给各路将领。用 abstract 修饰的类天然是 open 的,无需再添"开城令":

abstract class Shape {
    abstract val area: Double       // 抽象属性
    abstract fun draw()              // 抽象方法

    fun describe() {                  // 普通方法
        println("这是一个形状,面积为 $area")
    }
}

class Circle(val radius: Double) : Shape() {
    override val area: Double
        get() = Math.PI * radius * radius

    override fun draw() {
        println("画一个半径为 $radius 的圆")
    }
}

4.4 数据类 (data class) —— 军情档案

告别样板,速造档案

战场上,情报传递讲究快速准确。数据类就是 Kotlin 的"军情速递"——一个 data class 关键字,编译器便自动为你生成 equals()hashCode()toString()copy() 等全套方法。在 Java 阵营,同样的"档案"需要手写五六十行样板代码,而 Kotlin 一行定乾坤:

data class User(
    val id: Long,
    val name: String,
    val email: String
)

val user1 = User(1, "张三", "zhangsan@example.com")
val user2 = User(1, "张三", "zhangsan@example.com")

// 自动生成的 toString()
println(user1)  // User(id=1, name=张三, email=zhangsan@example.com)

// 自动生成的 equals() — 基于属性值比较
println(user1 == user2)  // true

// 自动生成的 copy() — 复制并修改部分属性
val user3 = user1.copy(name = "李四")
println(user3)  // User(id=1, name=李四, email=zhangsan@example.com)

三国鼎立:Kotlin vs Java vs C++

同一份军情档案,不同阵营的兵力消耗对比:

// Java: 需要大约 50-60 行代码
public class User {
    private final long id;
    private final String name;
    private final String email;

    public User(long id, String name, String email) { /* ... */ }
    public long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }

    @Override public boolean equals(Object o) { /* 十几行... */ }
    @Override public int hashCode() { /* ... */ }
    @Override public String toString() { /* ... */ }
    // 还要手写 copy 方法...
}

// Kotlin: 一行搞定
data class User(val id: Long, val name: String, val email: String)
C++ 开发者对照

C++ 开发者通常用 struct 来承载数据,但需要手动编写 operator==operator<< 等运算符重载。C++20 引入了三路比较运算符 operator<=>(太空船运算符),可以自动生成比较函数,但 Kotlin 的 data class 更为全面——自动生成 equals/hashCode/toString/copy 以及解构函数,无需任何额外声明。相当于 C++ 的 struct + operator== + operator<< + 拷贝构造函数 + 结构化绑定,一个 data 关键字全部搞定。

解构声明:分兵拆阵

data class 自动生成 componentN() 函数,支持解构声明——将一个阵型拆解为独立的单兵,各取所需:

data class Point(val x: Int, val y: Int)

val point = Point(10, 20)

// 解构声明: 将属性拆解到独立变量
val (x, y) = point
println("x=$x, y=$y")  // x=10, y=20

// 在遍历 Map 时特别好用
val map = mapOf("name" to "张三", "city" to "北京")
for ((key, value) in map) {
    println("$key = $value")
}

4.5 密封类 (sealed class) / 密封接口 —— 独门阵法

受限的类层次结构:封闭阵法

密封类乃兵家独门阵法——限定了继承它的子类必须定义在同一个包内(Kotlin 1.5+),犹如只有特定将领才能入阵。这让编译器能够穷举所有可能的子类型,做到"知己知彼,百战不殆":

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val message: String) : Result<Nothing>()
    data object Loading : Result<Nothing>()
}

与 when 表达式完美配合:天罗地网

密封类最大的优势在于配合 when 表达式时,编译器能确保你处理了所有情况——如同布下天罗地网,任何敌情都不会遗漏:

fun <T> handleResult(result: Result<T>) {
    when (result) {
        is Result.Success -> println("成功: ${result.data}")
        is Result.Error   -> println("错误: ${result.message}")
        Result.Loading    -> println("加载中...")
        // 不需要 else! 编译器知道已经穷举了所有情况
    }
}

军师锦囊

如果你后来给 Result 新增了一个子类,所有 when 表达式都会编译报错,提醒你处理新的情况。这比用 if-else 链安全得多——犹如每增加一路兵马,所有作战计划都会自动更新,绝不留死角。

C++ 开发者对照

Kotlin 的 sealed class 类似于 C++17 的 std::variant,都能表示"多选一"的类型。但 sealed class 更为优雅:编译器会在 when 表达式中强制穷举所有子类型,如果遗漏就报编译错误。而 C++ 的 std::visit 虽然也能做到穷举,但语法冗长且需要配合 overloaded 模式。此外,sealed class 的每个子类型可以拥有完全不同的属性结构,比 std::variant 的固定类型列表灵活得多。这正是"以简驭繁"的战略精髓。

实际用例:军情状态管理

密封类在 Android 开发中犹如军情快报系统,用来表示 UI 状态的变化——加载中、成功、失败,一目了然:

// 也可以用 sealed interface
sealed interface UiState<out T> {
    data object Loading : UiState<Nothing>
    data class Success<T>(val data: T) : UiState<T>
    data class Error(val exception: Throwable) : UiState<Nothing>
}

// 在 ViewModel 中使用
class UserViewModel : ViewModel() {
    private val _state = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
    val state = _state.asStateFlow()

    fun loadUsers() {
        viewModelScope.launch {
            _state.value = UiState.Loading
            try {
                val users = repository.getUsers()
                _state.value = UiState.Success(users)
            } catch (e: Exception) {
                _state.value = UiState.Error(e)
            }
        }
    }
}

4.6 枚举类 (enum class) —— 编制固定的兵种名册

基本用法

当你有一组固定不变的兵种编制时,枚举类就是最好的名册——人数固定,绝不超编:

enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

val dir = Direction.NORTH

when (dir) {
    Direction.NORTH -> println("向北")
    Direction.SOUTH -> println("向南")
    Direction.EAST  -> println("向东")
    Direction.WEST  -> println("向西")
}

带属性和方法的枚举

枚举中的每个兵种不仅有编号,还可以携带属性和方法——正如名册中记录了每位将士的武器和特技:

enum class Color(
    val rgb: Int,
    val displayName: String
) {
    RED(0xFF0000, "红色"),
    GREEN(0x00FF00, "绿色"),
    BLUE(0x0000FF, "蓝色");  // 注意: 最后一个枚举值后面要加分号

    // 枚举类中的方法
    fun hexString(): String = "#${rgb.toString(16).padStart(6, '0')}"
}

val color = Color.RED
println(color.displayName)  // 红色
println(color.hexString())  // #ff0000

// 遍历所有枚举值
Color.entries.forEach {
    println("${it.name}: ${it.displayName} (${it.hexString()})")
}

// 从字符串解析枚举
val fromString = Color.valueOf("GREEN")  // Color.GREEN

枚举 vs 密封类:名册与阵法之别

4.7 对象声明与伴生对象 —— 中军大帐与参谋部

object:中军大帐(单例模式一行搞定)

每支军队只有一个中军大帐——这就是单例模式。Kotlin 的 object 声明在语言层面直接支持单例,无需 Java 那套私有构造器、静态实例、双重检查锁的繁琐阵法:

object DatabaseConfig {
    val url = "jdbc:mysql://localhost:3306/mydb"
    val maxConnections = 10

    fun connect() {
        println("连接到 $url")
    }
}

// 直接通过类名访问,无需 getInstance()
DatabaseConfig.connect()
println(DatabaseConfig.maxConnections)  // 10

一个 object 关键字,便稳如泰山,线程安全,全局唯一——运筹帷幄,就是这般从容。

companion object:参谋部(类似 Java static 的替代)

Kotlin 没有 static 关键字。要实现类似功能,使用 companion object——它就像兵种蓝图里的"参谋部",不依附于具体士兵,而是服务于整个兵种:

class User private constructor(
    val id: Long,
    val name: String
) {
    companion object {
        private var nextId = 0L

        // 工厂方法
        fun create(name: String): User {
            return User(++nextId, name)
        }

        // "常量"
        const val MAX_NAME_LENGTH = 50
    }
}

// 使用: 看起来就像调用静态方法
val user = User.create("张三")
println(User.MAX_NAME_LENGTH)  // 50

参谋部的真正实力

companion object 实际上是一个单例对象,只不过它依附于外部类。它可以有名字,可以实现接口,还可以有扩展函数——这些都是 Java static 做不到的。犹如参谋部不仅出谋划策,还能独立执行任务。

C++ 开发者对照

Kotlin 没有 static 关键字,这一点与 C++ 差异极大。C++ 中 static 成员变量和方法属于类本身,而 Kotlin 用 companion object(伴生对象)来替代。关键区别在于:companion object 是一个真正的对象实例,它可以实现接口、被赋值给变量、拥有扩展函数——这些都是 C++ 的 static 成员做不到的。同时,Kotlin 的 object 声明替代了 C++ 中手写单例(Meyers' Singleton 等模式)的需求,天生线程安全,无需操心 std::call_once 或双重检查锁。

对象表达式:临时征召

有时你需要临时征召一批无名将士来执行特殊任务。Kotlin 用 object 表达式代替 Java 的匿名内部类:

// Java 风格的匿名内部类(在 Android 中很常见)
val listener = object : View.OnClickListener {
    override fun onClick(v: View) {
        println("按钮被点击了")
    }
}

// 也可以创建不继承任何类的匿名对象
val adHoc = object {
    val x = 10
    val y = 20
}
println("${adHoc.x}, ${adHoc.y}")  // 10, 20

军师锦囊

如果匿名内部类只有一个抽象方法(SAM 接口),Kotlin 可以用 Lambda 代替,更加简洁——以少胜多,方为上策:
button.setOnClickListener { println("点击!") }

4.8 可见性修饰符 —— 军事机密等级

战场上情报有机密等级,代码亦然。Kotlin 提供了四种可见性修饰符,犹如军中的四级保密制度:

修饰符 类成员 顶层声明 军事类比
public(默认) 到处可见 到处可见 公开军令,全军皆知
private 只在类内可见 只在当前文件内可见 最高机密,仅限本营
protected 类内 + 子类可见 不可用于顶层声明 指挥链密令,上下级共享
internal 同一模块内可见 同一模块内可见 战区内部通告,不外传
class BankAccount(private var balance: Double) {

    // public — 默认,到处可访问
    fun getBalance(): Double = balance

    // private — 只有类内部能调用
    private fun validate(amount: Double) {
        require(amount > 0) { "金额必须大于0" }
    }

    // protected — 类内和子类能调用
    protected open fun log(message: String) {
        println("[LOG] $message")
    }

    // internal — 同一模块(同一个 Gradle 模块)内可访问
    internal fun audit(): String = "余额: $balance"

    fun deposit(amount: Double) {
        validate(amount)
        balance += amount
        log("存入 $amount")
    }
}

三阵营可见性对比

特性 Kotlin Java C++
默认可见性 public package-private(包级私有) private(class)/ public(struct)
package-private 没有这个概念 默认可见性 没有对应概念(可用 friend)
internal 同一模块内可见 没有对应概念 没有对应概念
private(顶层) 文件级私有 不适用 类似匿名命名空间 / static
friend 没有此概念 没有此概念 允许指定类/函数访问 private 成员

战略要点

internal 修饰符在 Android 多模块项目中乃战略利器。它允许你在模块内自由调度 API,同时防止其他模块直接渗透内部实现。当你在设计 SDK 或公共库时,internal 是隐藏内部机密的铁壁——善守者,敌不知其所攻也。

本章小结

各位将军,本章的排兵布阵已到尾声。让我们回顾这张 OOP 战略全图:

记住军师之言:「先设计蓝图,再排兵布阵。」OOP 之精髓不在于写代码的速度,而在于架构设计的深度。下一章,我们将进入 Kotlin 的"杀手级特性"——空安全、扩展函数、Lambda 与高阶函数。那才是 Kotlin 真正亮剑的时刻。

本章练习

练习 1:定义兵种蓝图 入门

创建一个 Soldier 类,包含姓名(name: String)、攻击力(attack: Int)和防御力(defense: Int,默认值为 10)。使用主构造器定义属性,并添加一个 introduce() 方法打印士兵信息。创建两个 Soldier 对象并调用该方法。

提示

利用主构造器的 val 参数直接声明属性,使用默认参数值来简化构造。introduce() 方法中可用字符串模板 $name 来拼接输出。

参考答案
class Soldier(
    val name: String,
    val attack: Int,
    val defense: Int = 10
) {
    fun introduce() {
        println("士兵 $name: 攻击力=$attack, 防御力=$defense")
    }
}

val s1 = Soldier("赵云", 95, 88)
val s2 = Soldier("张飞", 98)  // defense 使用默认值 10
s1.introduce()  // 士兵 赵云: 攻击力=95, 防御力=88
s2.introduce()  // 士兵 张飞: 攻击力=98, 防御力=10

练习 2:建立指挥链(继承) 进阶

定义一个 open class Weapon(val name: String, val damage: Int),然后创建两个子类 Sword(近战武器,额外属性 length: Double)和 Bow(远程武器,额外属性 range: Int)。每个子类重写一个 open fun describe() 方法,输出武器的详细描述。

提示

父类需要标记为 open,方法也需要 open 才能被子类重写。子类构造器中,父类参数不加 val/var(因为已在父类中声明),子类自己的新属性才需要加。

参考答案
open class Weapon(val name: String, val damage: Int) {
    open fun describe() {
        println("武器: $name, 伤害: $damage")
    }
}

class Sword(
    name: String,
    damage: Int,
    val length: Double
) : Weapon(name, damage) {
    override fun describe() {
        println("近战武器 $name: 伤害=$damage, 长度=${length}m")
    }
}

class Bow(
    name: String,
    damage: Int,
    val range: Int
) : Weapon(name, damage) {
    override fun describe() {
        println("远程武器 $name: 伤害=$damage, 射程=${range}m")
    }
}

val sword = Sword("青龙偃月刀", 95, 2.1)
val bow = Bow("落日弓", 78, 150)
sword.describe()  // 近战武器 青龙偃月刀: 伤害=95, 长度=2.1m
bow.describe()    // 远程武器 落日弓: 伤害=78, 射程=150m

练习 3:军情速递(data class) 入门

创建一个 data class BattleReport,包含 location: String(战场)、result: String(结果)、casualties: Int(伤亡)。创建两个相同内容的 BattleReport 对象,验证 == 比较结果为 true,然后用 copy() 创建一个修改了 result 的副本。

提示

data class 自动生成基于属性值的 equals(),所以内容相同的两个对象比较结果为 truecopy() 方法可以使用命名参数修改部分属性。

参考答案
data class BattleReport(
    val location: String,
    val result: String,
    val casualties: Int
)

val r1 = BattleReport("赤壁", "胜利", 3000)
val r2 = BattleReport("赤壁", "胜利", 3000)

println(r1 == r2)  // true — 内容相同
println(r1)        // BattleReport(location=赤壁, result=胜利, casualties=3000)

val r3 = r1.copy(result = "惨败")
println(r3)        // BattleReport(location=赤壁, result=惨败, casualties=3000)

// 解构声明
val (loc, res, cas) = r1
println("$loc之战: $res, 伤亡 $cas")  // 赤壁之战: 胜利, 伤亡 3000

练习 4:设计独门阵法(sealed class) 挑战

设计一个 sealed class Command 来表示军队的指令系统,包含以下子类型:

  • Attack(val target: String, val troops: Int) — 进攻
  • Defend(val position: String) — 防守
  • Retreat — 撤退(使用 data object
  • Ambush(val location: String, val duration: Int) — 埋伏

然后编写一个 executeCommand(cmd: Command) 函数,使用 when 表达式处理每种指令并打印相应的执行信息。

提示

sealed class 的子类可以是 data class(携带数据)或 data object(单例,不携带数据)。when 表达式中使用 is 关键字匹配子类型,匹配后可以直接访问子类的属性(智能转换)。因为是 sealed class,不需要 else 分支。

参考答案
sealed class Command {
    data class Attack(
        val target: String,
        val troops: Int
    ) : Command()

    data class Defend(
        val position: String
    ) : Command()

    data object Retreat : Command()

    data class Ambush(
        val location: String,
        val duration: Int
    ) : Command()
}

fun executeCommand(cmd: Command) {
    when (cmd) {
        is Command.Attack ->
            println("进攻 ${cmd.target}! 出兵 ${cmd.troops} 人")
        is Command.Defend ->
            println("坚守 ${cmd.position} 阵地!")
        Command.Retreat ->
            println("鸣金收兵,全军撤退!")
        is Command.Ambush ->
            println("在 ${cmd.location} 设伏 ${cmd.duration} 小时")
    }
}

// 测试
executeCommand(Command.Attack("曹营", 5000))
executeCommand(Command.Defend("汉中"))
executeCommand(Command.Retreat)
executeCommand(Command.Ambush("华容道", 3))

练习 5:参谋部设计(companion object + 接口) 进阶

设计一个 interface Describable,包含一个 fun describe(): String 方法。然后创建一个 class Army,它的 companion object 实现 Describable 接口,并提供一个工厂方法 create(name: String, size: Int): Army。工厂方法应自动分配递增的 ID。

提示

companion object 可以实现接口——这是 Java static 做不到的。在 companion object 内部维护一个 private var nextId 作为自增 ID 计数器。构造器可以设为 private,强制通过工厂方法创建实例。

参考答案
interface Describable {
    fun describe(): String
}

class Army private constructor(
    val id: Int,
    val name: String,
    val size: Int
) {
    companion object : Describable {
        private var nextId = 0

        fun create(name: String, size: Int): Army {
            return Army(++nextId, name, size)
        }

        override fun describe(): String {
            return "Army 工厂: 已创建 $nextId 支军队"
        }
    }

    override fun toString(): String {
        return "Army #$id: $name (${size} 人)"
    }
}

val a1 = Army.create("虎豹骑", 5000)
val a2 = Army.create("白马义从", 3000)
println(a1)  // Army #1: 虎豹骑 (5000 人)
println(a2)  // Army #2: 白马义从 (3000 人)
println(Army.describe())  // Army 工厂: 已创建 2 支军队
← 上一章 目录 下一章 →