类、继承、接口、数据类、密封类 —— 以运筹帷幄之道,布局 Kotlin OOP 的战略蓝图
核心方法论:运筹帷幄,先设计蓝图再排兵布阵
「兵者,国之大事也。编程亦然。面向对象之道,犹如排兵布阵——类是你的兵种,继承是指挥链,接口是军令协议,密封类是独门阵法。不懂布局者,代码必乱如散兵;善于谋划者,架构自成铁壁。今日,吾将以军略之道,带各位将军领悟 Kotlin OOP 的精髓。切记:先画蓝图,再动代码。未战而庙算者胜!」
三军未动,兵种先行。在 Kotlin 的战场上,类就是你麾下的兵种蓝图。定义一个兵种可以极其简洁——构造器参数直接写在类名后面,加上 val 或 var 就自动成为该兵种的属性,无需冗余的样板代码:
class Person(val name: String, var age: Int)
// 使用
val person = Person("张三", 25)
println(person.name) // 张三
person.age = 26 // var 可以修改
// person.name = "李四" // 编译错误! val 不可修改
仅此一行,Kotlin 便为你铸造了一个包含两个属性、一个构造器的完整兵种蓝图。正所谓化繁为简,上兵伐谋。
类名后面跟的便是主构造器(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 块就是你的"练兵场"——在对象创建时执行初始化逻辑,确保每一个新兵都合格:
class Person(val name: String, var age: Int) {
init {
require(age >= 0) { "年龄不能为负数" }
println("创建了一个 Person: $name, $age 岁")
}
// 可以有多个 init 块,按顺序执行
init {
println("第二个 init 块")
}
}
让我们以三国之势,看看同样的兵种蓝图在不同阵营中需要多少笔墨:
// 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++ 的类默认可以被继承(除非标记为 final),而 Kotlin 的类默认就是 final 的,必须加 open 关键字才允许继承。这正如兵法所云:「善守者,藏于九地之下」——Kotlin 选择了默认封闭的防御策略,只在你明确需要时才开放继承。C++ 则是默认敞开城门,需要你自己加 final 去守卫。此外,Kotlin 的主构造器语法将参数声明与属性定义合二为一,省去了 C++ 中构造函数初始化列表和成员变量声明的繁琐。
在排兵布阵中,每个兵种都有其核心属性——攻击力、防御力、速度。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 就是你的"附魔工坊"——在读写属性时注入自定义逻辑:
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
在自定义 setter 中,field 是所谓的幕后字段(backing field),犹如军中密令,代表属性实际存储的值。切不可混淆——用属性名赋值会陷入"无限递归"的圈套:
var counter: Int = 0
set(value) {
if (value >= 0) {
field = value // field 指向真正存储值的地方
}
// 如果写 this.counter = value 会无限递归调用 setter!
}
军令警告
在自定义 setter 中,一定要用 field 而不是属性名来赋值,否则会导致无限递归,最终 StackOverflowError。此乃兵家大忌,如同"追击敌军却绕回自家营寨",死循环矣!
兵马未动,粮草先行——但有些"装备"无法在出征前备齐。比如 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 军规
var,不能用于 valInt、Boolean 等)UninitializedPropertyAccessException——如同未备粮草就强行出征,必败!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 绑定 | 昂贵资源的延迟加载 |
兵法云:「善守者,敌不知其所攻。」与 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++ 支持多重继承,但由此带来了臭名昭著的菱形继承问题(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 的圆")
}
}
战场上,情报传递讲究快速准确。数据类就是 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)
同一份军情档案,不同阵营的兵力消耗对比:
// 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++ 开发者通常用 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")
}
密封类乃兵家独门阵法——限定了继承它的子类必须定义在同一个包内(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 表达式时,编译器能确保你处理了所有情况——如同布下天罗地网,任何敌情都不会遗漏:
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 链安全得多——犹如每增加一路兵马,所有作战计划都会自动更新,绝不留死角。
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)
}
}
}
}
当你有一组固定不变的兵种编制时,枚举类就是最好的名册——人数固定,绝不超编:
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 密封类:名册与阵法之别
每支军队只有一个中军大帐——这就是单例模式。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 关键字,便稳如泰山,线程安全,全局唯一——运筹帷幄,就是这般从容。
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 做不到的。犹如参谋部不仅出谋划策,还能独立执行任务。
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("点击!") }
战场上情报有机密等级,代码亦然。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 战略全图:
val/var 参数自动成为属性,化繁为简,上兵伐谋。lateinit(先声后装)和 by lazy(以逸待劳)。final(铁壁防御),需要 open 才能继承;接口可以有默认实现。equals()、hashCode()、toString()、copy() 和解构声明。when 完美配合,布下天罗地网,不留遗漏。static 替代方案。public,新增 internal(战区级),没有 package-private。记住军师之言:「先设计蓝图,再排兵布阵。」OOP 之精髓不在于写代码的速度,而在于架构设计的深度。下一章,我们将进入 Kotlin 的"杀手级特性"——空安全、扩展函数、Lambda 与高阶函数。那才是 Kotlin 真正亮剑的时刻。
创建一个 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
定义一个 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
创建一个 data class BattleReport,包含 location: String(战场)、result: String(结果)、casualties: Int(伤亡)。创建两个相同内容的 BattleReport 对象,验证 == 比较结果为 true,然后用 copy() 创建一个修改了 result 的副本。
data class 自动生成基于属性值的 equals(),所以内容相同的两个对象比较结果为 true。copy() 方法可以使用命名参数修改部分属性。
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
设计一个 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))
设计一个 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 支军队