编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

    • Kotlin精通

      • Kotlin精通模块总览
      • Kotlin基础语法
      • 类和对象
        • 2.1 类的介绍
          • 2.1.1 类的含义
          • 2.1.2 Kotlin与Java差异
          • 2.1.3 默认final的哲学
          • 2.1.4 综合案例与思考
        • 2.2 类定义与实例化
          • 2.2.1 主构造函数
          • 2.2.2 次构造函数
          • 2.2.3 init初始化块
          • 2.2.4 无new关键字
          • 2.2.5 综合案例与思考
        • 2.3 属性与字段
          • 2.3.1 属性并非字段
          • 2.3.2 自定义getter
          • 2.3.3 field幕后字段
          • 2.3.4 lateinit与lazy
          • 2.3.5 综合案例与思考
        • 2.4 可见性修饰符
          • 2.4.1 四种可见性
          • 2.4.2 internal是什么
          • 2.4.3 顶层可见性
          • 2.4.4 综合案例与思考
        • 2.5 特殊类家族
          • 2.5.1 数据类data
          • 2.5.2 密封类sealed
          • 2.5.3 枚举类enum
          • 2.5.4 单例object
          • 2.5.5 伴生对象companion
          • 2.5.6 内联类value
          • 2.5.7 综合案例与思考
        • 2.6 继承与接口
          • 2.6.1 open显式打开
          • 2.6.2 override强制标注
          • 2.6.3 接口默认方法
          • 2.6.4 抽象类vs接口
          • 2.6.5 综合案例与思考
        • 2.7 委托机制
          • 2.7.1 类委托by
          • 2.7.2 属性委托by
          • 2.7.3 内建委托家族
          • 2.7.4 综合案例与思考
        • 2.8 泛型与型变
          • 2.8.1 泛型类与函数
          • 2.8.2 in与out型变
          • 2.8.3 reified具化类型
          • 2.8.4 where约束语法
          • 2.8.5 综合案例与思考
        • 2.9 综合案例练习
          • 2.9.1 电商订单系统
          • 2.9.2 状态机建模
        • 2.10 类和对象底层原理
          • 2.10.1 属性编译成什么
          • 2.10.2 data类生成物
          • 2.10.3 object的字节码
          • 2.10.4 委托的字节码
        • 2.11 训练题三道
        • 2.12 新手陷阱Top5
      • 高阶特性入门
      • 空安全类型原理
      • data类与equals原理
      • 协程调度器原理
      • inline与reified原理
      • 委托与属性原理
    • 库的解读

    • 专栏博客

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

  • IoT智能硬件开发

  • Apps
  • Android提升进阶
  • Kotlin精通
杨充
2026-07-03
目录

类和对象

# 第 2 章 Kotlin 类和对象

# 目录介绍

  • 2.1 类的介绍
    • 2.1.1 类的含义
    • 2.1.2 Kotlin与Java差异
    • 2.1.3 默认final的哲学
    • 2.1.4 综合案例与思考
  • 2.2 类定义与实例化
    • 2.2.1 主构造函数
    • 2.2.2 次构造函数
    • 2.2.3 init初始化块
    • 2.2.4 无new关键字
    • 2.2.5 综合案例与思考
  • 2.3 属性与字段
    • 2.3.1 属性并非字段
    • 2.3.2 自定义getter
    • 2.3.3 field幕后字段
    • 2.3.4 lateinit与lazy
    • 2.3.5 综合案例与思考
  • 2.4 可见性修饰符
    • 2.4.1 四种可见性
    • 2.4.2 internal是什么
    • 2.4.3 顶层可见性
    • 2.4.4 综合案例与思考
  • 2.5 特殊类家族
    • 2.5.1 数据类data
    • 2.5.2 密封类sealed
    • 2.5.3 枚举类enum
    • 2.5.4 单例object
    • 2.5.5 伴生对象companion
    • 2.5.6 内联类value
    • 2.5.7 综合案例与思考
  • 2.6 继承与接口
    • 2.6.1 open显式打开
    • 2.6.2 override强制标注
    • 2.6.3 接口默认方法
    • 2.6.4 抽象类vs接口
    • 2.6.5 综合案例与思考
  • 2.7 委托机制
    • 2.7.1 类委托by
    • 2.7.2 属性委托by
    • 2.7.3 内建委托家族
    • 2.7.4 综合案例与思考
  • 2.8 泛型与型变
    • 2.8.1 泛型类与函数
    • 2.8.2 in与out型变
    • 2.8.3 reified具化类型
    • 2.8.4 where约束语法
    • 2.8.5 综合案例与思考
  • 2.9 综合案例练习
    • 2.9.1 电商订单系统
    • 2.9.2 状态机建模
  • 2.10 类和对象底层原理
    • 2.10.1 属性编译成什么
    • 2.10.2 data类生成物
    • 2.10.3 object的字节码
    • 2.10.4 委托的字节码
  • 2.11 训练题三道
  • 2.12 新手陷阱Top5

# 2.1 类的介绍

# 2.1.1 类的含义

类是一种用户自定义类型,用于封装 属性(数据)与 行为(函数)。Kotlin 类可以看作 Java 类的「精简升级版」——同样的语义,一半的代码。

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

这一行不到 40 个字符,等价于下面 Java 十几行 boilerplate:

public final 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 编译器背后仍会生成同样的字节码——这是「零成本抽象」——你没有写的 getter/setter 是 编译器帮你写的,运行期性能与 Java 完全一致。第 2.10 节我们会亲眼看到 javap 反编译的证据。

类是 OOP 的最小单位:它把「数据 + 操作数据的方法」封装到同一个类型里,让「什么样的数据能做什么样的操作」由类型系统保证。类同时给编译器一个明确的作用域,可见性、继承规则、命名空间都在类的粒度上生效。

# 2.1.2 Kotlin与Java差异

Kotlin 保留了 Java 的类模型(单继承 + 多接口 + JVM 字节码),但从 默认值 和 语法糖 两个层面做了大量修正:

维度 Java Kotlin
默认可继承 是(可 extends) 否(默认 final)
默认可见性 package-private public
字段与访问器 手写 getter/setter 属性自带
单一继承 + 多接口 一致 一致
多构造 多个 public XXX(...) 主构造 + 次构造
静态成员 static 关键字 companion object
数据类 自己写或 Lombok data class 一行
枚举 传统 enum enum class(可属性、方法、抽象方法)
密封类 Java 17+ 才有 sealed Kotlin 1.0 就支持
委托 手写 or Proxy by 关键字语言级支持
实例化 必须 new 无 new

「默认值差异」是最容易踩坑的部分——写 Java 十年的人第一次写 Kotlin,最常见的错误就是忘了给类加 open 导致无法继承、以及忘了给成员加 open 导致无法 override。

# 2.1.3 默认final的哲学

Kotlin 借鉴 Joshua Bloch《Effective Java》条款 17:"要么设计好继承并写好文档,要么禁止继承"。所以默认给你 final——想继承请显式写 open。

class Base           // 默认 final,不能继承
open class Base2     // 显式打开,才能继承

动机:可继承是一种「向所有子类做出的契约承诺」。任何非 final 的方法都必须假设「未来的子类可能重写它」,从而在实现里避免副作用泄漏、状态不一致、循环调用等风险。Java 默认可继承,导致大量库作者被迫在方法上加 final,或者被子类踩坑——Kotlin 直接把默认反过来。

与 Java 15+ sealed class 的对齐:Java 15 引入 sealed 也是同样思路——「继承需要显式允许」。Kotlin 走得更彻底:sealed 是「限定继承者集合」,而 final 是「禁止继承」,open 是「允许任何继承」,三者形成完整梯度。

代价:一些依赖 Mock 的测试框架(Mockito)无法 mock 默认 final 类。Kotlin 官方给出的方案是 all-open (opens new window) 编译器插件(Spring/JPA 场景已经内建):可以让特定注解标注的类默认 open,测试时再全开。

# 2.1.4 综合案例与思考

Java Bean 逐行迁移案例——把下面这个典型 Java Bean:

public class Book {
    private String title;
    private String author;
    private int pages;

    public Book(String title, String author, int pages) {
        this.title = title;
        this.author = author;
        this.pages = pages;
    }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public int getPages() { return pages; }
    public void setPages(int pages) { this.pages = pages; }

    @Override public boolean equals(Object o) { /* IDE 生成的 20 行 */ }
    @Override public int hashCode()          { /* 5 行 */ }
    @Override public String toString()       { /* 3 行 */ }
}

用 Kotlin 重写:

data class Book(var title: String, var author: String, var pages: Int)

行数对比:Java 40+ 行 → Kotlin 1 行,行数缩水 97.5%。而且 equals / hashCode / toString / copy / componentN 是编译器自动生成的、永远与字段一致——Java 里如果新加字段忘了同步 equals,就是隐藏的等价性 bug。

思考:为什么 Kotlin 把默认可见性从 Java 的 package-private 改成 public?

原因:package-private 的实际使用率极低,Google 内部代码统计不到 2%。相反,public 是绝大多数类的默认需求。让默认值贴近实际最常用的场景,能减少 90% 的 public 关键字噪音。想收窄可见性时再显式写 internal / private。


# 2.2 类定义与实例化

# 2.2.1 主构造函数

class Person(val name: String, var age: Int) {
    // 类体
}

主构造 写在类头,参数可以直接加 val/var 变成属性(一石二鸟)。规则:

头声明 含义
class C(x: Int) x 只是构造参数,不是属性;类体外不可见
class C(val x: Int) x 是只读属性,自带 getter
class C(var x: Int) x 是读写属性,自带 getter/setter

注意:主构造不能包含代码,只能声明参数。想在构造时执行代码,请使用 init 块(2.2.3)。

默认参数 是主构造的常见搭档,让 Java 里必须写多个重载构造的场景在 Kotlin 里一行搞定:

class Http(
    val url: String,
    val method: String = "GET",
    val timeout: Int = 30,
    val retries: Int = 3,
)

val h1 = Http("https://x.com")                            // 全用默认值
val h2 = Http("https://x.com", timeout = 60)              // 命名参数改一个
val h3 = Http("https://x.com", "POST", retries = 5)       // 位置 + 命名混用

Java 里同样效果需要写 8 个 重载构造——2^3 种组合。

# 2.2.2 次构造函数

class Person(val name: String) {
    var age: Int = 0
    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
}

规则:次构造必须直接或间接调用主构造。语法:constructor(...) : this(...)。

次构造 vs 默认参数:Kotlin 90% 的场景推荐默认参数——只有一种情况必须写次构造:从其他类型转换。例如:

class Point(val x: Int, val y: Int) {
    constructor(pair: Pair<Int, Int>) : this(pair.first, pair.second)
    constructor(list: List<Int>) : this(list[0], list[1])
}

上面的例子无法用默认参数表达——因为参数类型不同。

# 2.2.3 init初始化块

class Person(val name: String) {
    init {
        require(name.isNotBlank()) { "name不能为空" }
    }
}

init 块在主构造后、次构造前执行;多个 init 按声明顺序运行。执行时序:

1. 求值主构造参数
2. 依次执行【属性初始化】和【init块】——按源码顺序交织
3. 执行次构造函数体(如果走的是次构造)

顺序陷阱:

class Bad(val n: Int) {
    init { println(a) }     // ❌ 编译错:a 还没声明
    val a = n * 2
}

init 里访问 a 会报错,因为 a 的声明还在下面。记住:init 块和属性声明按源码从上到下依赖,不能倒序。

# 2.2.4 无new关键字

val p = Person("Kotlin", 10)   // 没有 new

Kotlin 抛弃了 new——少一个关键字,语法更接近函数调用。

设计动机:让 工厂函数、构造函数、单例访问器 在调用侧写法一致:

val a = ArrayList<Int>()        // 构造函数
val b = arrayListOf(1, 2, 3)    // 工厂函数
val c = AppConfig               // 单例

三种「获取对象」的方式,调用侧看不出区别。这样类的作者可以自由地在「构造」和「工厂」之间切换,调用者代码零改动——例如把 class Foo(...) 改成 fun Foo(...): Foo 工厂函数,调用侧完全无感。

# 2.2.5 综合案例与思考

HttpRequest 类:必填参数放主构造,可选参数放默认参数,特殊场景放次构造:

class HttpRequest(
    val url: String,                          // 必填
    val method: String = "GET",               // 可选
    val headers: Map<String, String> = emptyMap(),
    val body: ByteArray? = null,
    val timeoutMs: Int = 30_000,
) {
    // 特殊:从 URL 字符串(含 query)快捷构造
    constructor(url: String, params: Map<String, String>) : this(
        url = "$url?" + params.entries.joinToString("&") { "${it.key}=${it.value}" }
    )

    init {
        require(url.startsWith("http")) { "只支持 HTTP(S)" }
        require(timeoutMs > 0) { "超时必须 > 0" }
    }

    override fun toString() = "$method $url"
}

fun main() {
    val r1 = HttpRequest("https://api.demo.com/users")
    val r2 = HttpRequest("https://api.demo.com/users", method = "POST", timeoutMs = 60_000)
    val r3 = HttpRequest("https://api.demo.com/search", mapOf("q" to "kotlin", "page" to "1"))
    println(r1); println(r2); println(r3)
}

输出:

GET https://api.demo.com/users
POST https://api.demo.com/users
GET https://api.demo.com/search?q=kotlin&page=1

思考:主构造 + 默认参数 vs 多个次构造,什么时候用哪种?

判断标准:如果只是「参数个数不同」——用默认参数;如果是「参数类型完全不同」——用次构造。前者是「同一个语义的多种简写」,后者是「不同来源转换到同一个对象」。


# 2.3 属性与字段

# 2.3.1 属性并非字段

关键观念:Kotlin 只有「属性」没有「字段」。val name 声明的是一个 属性(自带 getter),编译器 可能 为它生成一个私有字段(叫 backing field)。

class Person(val firstName: String, val lastName: String) {
    val fullName: String
        get() = "$firstName $lastName"   // 计算属性,没有 backing field
}

判断规则:只有当 getter/setter 里出现 field 关键字(或采用默认实现)时,编译器才生成 backing field。计算属性 fullName 每次调用都重新拼接,不占内存。

为什么这个区别重要——Java 程序员的第一个 Kotlin 陷阱:

class Counter {
    var count: Int = 0
        set(value) {
            count = value + 1   // ❌ 栈溢出!递归调用 setter
        }
}

赋值 count = value + 1 会调用 setter 本身——无限递归。正确写法是用 field(2.3.3)。

# 2.3.2 自定义getter

class Rect(val w: Int, val h: Int) {
    val area: Int
        get() = w * h
}

用法场景:值 可派生 时用计算属性,值 需要缓存 时用 backing field + by lazy。

性能提示:如果计算开销大且反复访问——用 by lazy 而不是自定义 getter,否则每次访问都算一遍。

class Report(val rows: List<Row>) {
    val total: Int by lazy {
        rows.sumOf { it.amount }         // 只算一次
    }
}

# 2.3.3 field幕后字段

field 是 只能 在自定义 getter/setter 里使用的关键字,指向属性的 backing field:

class Age {
    var value: Int = 0
        set(newValue) {
            require(newValue >= 0) { "年龄不能为负" }
            field = newValue         // 用 field,不要用 value
        }
}

为什么必须用 field?——因为 setter 是通过 set... 方法名调用的,在 setter 内直接给 value 赋值会解析成 setValue(...)——递归调用自己,导致栈溢出。

编译器视角:Kotlin 编译器看到 field 关键字,就把它翻译成对私有字段的直接读写;看到属性名,就翻译成 getter/setter 调用。两种访问方式一定要分开。

# 2.3.4 lateinit与lazy

修饰 适用 语义
lateinit var 非空引用类型(不含基础类型) 保证使用前会赋值,运行时检查
by lazy { } val 首次访问时计算,之后缓存
class UserVM {
    lateinit var repo: UserRepo             // 依赖注入前必须赋值
    val config: Config by lazy { loadConfig() }
}

lateinit 三条限制:

  1. 必须是 var(因为要延迟赋值)
  2. 必须是非空引用类型(不能是 Int / Boolean 等基础类型——用 by Delegates.notNull() 替代)
  3. 必须有 backing field(不能是计算属性)

运行期检查:如果访问一个未初始化的 lateinit var,会抛 UninitializedPropertyAccessException。可以用 ::propName.isInitialized 先判断:

if (::repo.isInitialized) repo.load()

by lazy 三种线程模式(详见原理篇 5):

模式 说明
LazyThreadSafetyMode.SYNCHRONIZED(默认) 加锁,多线程安全,只计算一次
LazyThreadSafetyMode.PUBLICATION 无锁,可能多个线程各算一次,取第一个成功值
LazyThreadSafetyMode.NONE 无同步,单线程用,最快

# 2.3.5 综合案例与思考

Temperature 类——属性内部做单位换算,展示 field 与自定义 setter:

class Temperature {
    var celsius: Double = 0.0            // 主存储:摄氏度

    var fahrenheit: Double
        get() = celsius * 9 / 5 + 32
        set(value) {
            celsius = (value - 32) * 5 / 9   // 内部转成摄氏度
        }

    var kelvin: Double
        get() = celsius + 273.15
        set(value) {
            require(value >= 0) { "开尔文温度不能为负" }
            celsius = value - 273.15
        }
}

fun main() {
    val t = Temperature()
    t.celsius = 25.0
    println("摄氏 25 → 华氏 ${t.fahrenheit} → 开尔文 ${t.kelvin}")
    t.fahrenheit = 100.0
    println("华氏 100 → 摄氏 ${t.celsius}")
}

输出:

摄氏 25 → 华氏 77.0 → 开尔文 298.15
华氏 100 → 摄氏 37.77777777777778

思考:什么时候属性该有 backing field?编译器如何判断?

判断规则:Kotlin 编译器扫描 getter 和 setter,只要出现 field 关键字(或采用默认实现,即没写 getter/setter),就生成 backing field;否则不生成,属性变成纯派生。


# 2.4 可见性修饰符

# 2.4.1 四种可见性

修饰符 类内成员 顶层声明
public(默认) 处处可见 处处可见
internal 同 module 可见 同 module 可见
protected 类内与子类 ❌ 顶层不可用
private 类内 同文件 内可见

注意 private 的两种语义:

  • 类内成员的 private——只在类内可见
  • 顶层声明的 private——只在 同一个 .kt 文件 内可见

这是 Kotlin 独有的「文件级 private」,非常适合把辅助函数和主类放在同一个文件里,且不污染包命名空间。

# 2.4.2 internal是什么

Kotlin 独有——表示「同 module 可见」。module 定义为:一起编译的一组 Kotlin 文件(一个 Gradle module / IntelliJ module / Maven project)。

用法典型场景:SDK / 库项目

// 对外 API
class NetworkClient {
    fun fetch(url: String): String = engine.execute(url)
    internal val engine = HttpEngine()   // 内部使用,外部不可见
}

// 实现细节,同 module 可见
internal class HttpEngine {
    fun execute(url: String): String = ...
}

Java 侧调用体验:internal 修饰的成员会被 Kotlin 编译器 name mangling,编译后名字变成 HttpEngine$module_release 之类,Java 代码理论上仍能访问但看起来非常丑陋——这就是 Kotlin 「劝退」Java 直接调 internal 的方式。

internal vs Java 的 package-private:

维度 Java package-private Kotlin internal
边界 包(package) module
强制性 包名相同就可访问,容易被利用 无法从其他 module 访问
检查时机 编译期 编译期
Kotlin 是否保留 否 是

# 2.4.3 顶层可见性

Kotlin 允许 顶层函数(不在任何类里的函数),它们的可见性规则参考类内成员:

// file: utils.kt
fun publicHelper() { }              // 默认 public,全局可见
internal fun sharedHelper() { }     // 同 module 可见
private fun fileHelper() { }        // 只在 utils.kt 内可见
// protected 不可用(无「子类」概念)

顶层函数编译成 UtilsKt 这个类的静态方法,Java 侧调用是 UtilsKt.publicHelper()。想给 Java 起个别名,加 @file:JvmName("Utils")。

# 2.4.4 综合案例与思考

SDK 设计场景:

// ============ 对外 API 文件 ============
// file: PaySdk.kt
class PaySdk {
    fun pay(orderId: String, amount: Long): PayResult {
        val encoded = encoder.encode(orderId, amount)
        return client.send(encoded)
    }

    // 内部字段
    internal val encoder = OrderEncoder()
    internal val client = PayClient()
}

// ============ 内部实现文件 ============
// file: OrderEncoder.kt
internal class OrderEncoder {
    fun encode(orderId: String, amount: Long): ByteArray = ...
}

// file: PayClient.kt
internal class PayClient {
    fun send(data: ByteArray): PayResult = ...
}

对外只暴露 PaySdk 一个类,OrderEncoder / PayClient 是内部实现——用 internal 让实现细节 同 module 可用(便于测试),但对使用 SDK 的外部业务代码 完全不可见。

思考:为什么 Kotlin 抛弃了 Java 的 package-private?

原因:Java 的 package-private 用「包名」做隔离边界——但 Java 包名并不安全,任何人只要写一个相同包名的 .java 文件,就能访问 package-private 成员。Kotlin 用 module 作为隔离边界,天然是编译单元,无法绕过。


# 2.5 特殊类家族

Kotlin 特殊类是 语言级糖,每一个都对应一个具体的编译产物。理解它们「是什么、编译成什么、什么时候用」是精通 Kotlin 类型系统的关键。

# 2.5.1 数据类data

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

编译器自动生成:

生成方法 语义
equals(other: Any?) 按主构造属性逐个比较
hashCode() 按主构造属性组合计算
toString() User(id=..., name=...)
copy(...) 复制并可选择性覆盖属性
component1() / component2() / ... 解构声明支持

解构声明:

val u = User(1L, "Kotlin")
val (id, name) = u          // 幕后:id = u.component1(); name = u.component2()
println("$id $name")        // 输出: 1 Kotlin

copy 的威力——不可变数据流场景(如 Compose UI State):

data class State(val loading: Boolean = false, val list: List<Item> = emptyList())

var state = State()
state = state.copy(loading = true)            // 只改 loading
state = state.copy(loading = false, list = fetched)

详细字节码见 原理篇 2。

# 2.5.2 密封类sealed

sealed class Result<out T>
class Success<T>(val data: T) : Result<T>()
class Failure(val err: Throwable) : Result<Nothing>()
object Loading : Result<Nothing>()

核心特性:所有直接子类必须在 同文件(或同 module,Kotlin 1.5+)。让 when 表达式可穷尽性检查:

fun <T> render(r: Result<T>): String = when (r) {   // 编译器要求穷尽所有分支
    is Success -> "OK: ${r.data}"
    is Failure -> "ERR: ${r.err.message}"
    is Loading -> "..."
}

如果新增一个子类,所有的 when 都会 编译报错,提示你补上分支——这就是「代数数据类型」的类型安全保证。

sealed class vs enum class 取舍:

维度 enum sealed
子类数量 有限、编译期固定 有限、编译期固定
子类是否可携带 不同类型的数据 ❌ 所有实例结构一致 ✅ 每个子类可有各自属性
子类是否可以有多个实例 ❌ 每个枚举值只有一份 ✅ class Success<T>(...) 每次调都是新实例
典型场景 状态、类型、常量 结果、事件、消息

# 2.5.3 枚举类enum

enum class Color(val hex: String) {
    RED("#FF0000"), GREEN("#00FF00"), BLUE("#0000FF");

    fun brightness() = hex.substring(1).toInt(16)
}

fun main() {
    for (c in Color.values()) {
        println("${c.name} ${c.hex} brightness=${c.brightness()}")
    }
}

输出:

RED #FF0000 brightness=16711680
GREEN #00FF00 brightness=65280
BLUE #0000FF brightness=255

Kotlin 枚举 = Java 枚举,编译目标一致,但语法更整洁——每个值可以带自己的构造参数,还可以定义方法、抽象方法:

enum class Op {
    ADD { override fun apply(a: Int, b: Int) = a + b },
    SUB { override fun apply(a: Int, b: Int) = a - b };

    abstract fun apply(a: Int, b: Int): Int
}

# 2.5.4 单例object

object AppConfig {
    var debug = false
    fun load() { }
}

AppConfig.debug = true       // 直接用类名访问

object 声明的类 只有一个实例,编译成带 INSTANCE 静态字段的 final class。等价于 Java 的:

public final class AppConfig {
    public static final AppConfig INSTANCE = new AppConfig();
    private AppConfig() {}
    // ...
}

线程安全:INSTANCE 是在类加载时初始化的——JVM 类加载器保证同一个类只被加载一次,天然线程安全。这比 Java 的「双重检查锁定」单例模式简洁 10 倍。

object 三种用法:

// 1. 顶层 object:全局单例
object Logger { fun d(msg: String) { } }

// 2. 类内 object:命名嵌套单例
class Http {
    object Constants { const val TIMEOUT = 30 }
}
// 用法:Http.Constants.TIMEOUT

// 3. 对象表达式:匿名单例
val listener = object : ClickListener {
    override fun onClick() { }
}

# 2.5.5 伴生对象companion

class User private constructor(val id: Long) {
    companion object {
        fun of(id: Long) = User(id)
        const val ANONYMOUS_ID = -1L
    }
}

val u = User.of(42)

伴生对象是宿主类的一个内部单例,用来放「类级」的工厂/常量。不是 Java 的 static——不加 @JvmStatic 时 Java 侧调用要写:

User.Companion.of(42);              // Java 视角

加上 @JvmStatic 后 Java 就能直接 User.of(42):

class User private constructor(val id: Long) {
    companion object {
        @JvmStatic fun of(id: Long) = User(id)
    }
}

为什么设计成 companion object 而不是 static——因为 companion 是一个 真正的对象,可以:

  • 实现接口(companion object : Factory<User>)
  • 有属性(每次访问都走 getter)
  • 被扩展(fun User.Companion.random() = ...)

这些 Java 的 static 都做不到。

# 2.5.6 内联类value

@JvmInline
value class UserId(val value: Long)

fun findUser(id: UserId): User? { ... }
fun findOrder(id: OrderId): Order? { ... }

val userId = UserId(100)
val orderId = OrderId(200)
findUser(userId)          // ✅
findUser(orderId)         // ❌ 编译错误——类型不同

编译后运行时是 裸 Long,不装箱、不多一次分配——用来给基础类型加上类型安全。Kotlin 1.5 稳定。

核心用途:给「同样是 Long 但语义不同」的字段加上类型区分。传统方式:

fun findUser(id: Long)     // Long 类型的 userId
fun findOrder(id: Long)    // Long 类型的 orderId
findUser(orderId)          // ❌ 想混用编译器也不报错——运行期灾难

用了 value class 之后,编译期直接拦截错误。

限制:value class 只能有 一个 主构造属性(1.9 支持多个但还在实验);不能有 init 块之外的属性;不能有 backing field 之外的属性。

# 2.5.7 综合案例与思考

网络请求 Result 状态机——sealed + data class 组合建模:

sealed class Resource<out T> {
    object Loading : Resource<Nothing>()
    data class Success<T>(val data: T) : Resource<T>()
    data class Error(val code: Int, val msg: String) : Resource<Nothing>()
}

fun handle(r: Resource<List<User>>) = when (r) {
    Resource.Loading -> showLoading()
    is Resource.Success -> showList(r.data)
    is Resource.Error -> showToast("${r.code}: ${r.msg}")
}

思考:object、companion object、class ... { object ... } 三者语义分别是什么?

答案:

  • 顶层 object A:全局单例,A.method() 调用
  • companion object:宿主类的伴生单例,Host.method() 调用(本质是 Host.Companion.method())
  • 类内 object A:宿主类的命名嵌套单例,Host.A.method() 调用,每个宿主类实例共享同一个 A

# 2.6 继承与接口

# 2.6.1 open显式打开

open class Animal {
    open fun speak() { println("...") }
    fun eat() { }                     // 未 open,子类不能 override
}

class Dog : Animal() {                 // 冒号 + 圆括号=调用父类构造
    override fun speak() { println("汪") }
}

必须 加 open 才能被继承或 override——与 Java 「默认可继承」相反。

为什么调父类构造要用括号:Animal() 是「调用 Animal 的无参构造函数」,如果 Animal 有参构造,就写 class Dog : Animal(name, age)。

# 2.6.2 override强制标注

Kotlin 强制加 override 关键字,签名错误会立即报错,避免 Java 的「静默失败」:

// Java:忘了 @Override,签名写错,编译通过
public class Dog extends Animal {
    public void speek() { }        // 打错字,成了 speek——不是 override
}
// Kotlin:必须写 override
class Dog : Animal() {
    override fun speek() { }       // ❌ 编译错:'speek' 在父类找不到
    override fun speak() { }       // ✅
}

override 关键字还有个副作用:默认自动 open——子类的 override 方法可以被孙类再次 override。想禁止,加 final override:

open class Base { open fun foo() {} }
open class Middle : Base() {
    final override fun foo() {}    // Middle.foo 不允许再被继承者 override
}

# 2.6.3 接口默认方法

interface Animal {
    fun speak()
    fun greet() = println("Hi")    // 默认方法
    val species: String            // 接口属性(抽象)
        get() = "unknown"          // 有默认 getter 也可以
}

Kotlin 接口天然支持默认方法,甚至可以有属性(无 backing field)——因为接口不能持有状态,属性只能通过 getter 派生。

多接口冲突 用 super<A>.foo() 消歧义:

interface A { fun x() = println("A.x") }
interface B { fun x() = println("B.x") }

class C : A, B {
    override fun x() {
        super<A>.x()      // 显式调 A 的默认实现
        super<B>.x()      // 显式调 B 的默认实现
    }
}

# 2.6.4 抽象类vs接口

需求 选择
需要状态(backing field) 抽象类
多重实现能力 接口
二者都要 接口做行为、抽象类做骨架

具体决策标准:

  • 想给类 加上一种能力(可点击、可比较、可迭代)——用 接口
  • 想让一组类 共享同一份实现代码——用 抽象类
  • 类是 必然的一部分 还是 偶尔的一部分——前者用抽象类,后者用接口
// 抽象类:所有 Shape 共享 area 属性和 log 逻辑
abstract class Shape {
    abstract val area: Double
    fun log() = println("area=$area")
}

// 接口:任何东西都可以被点击
interface Clickable {
    fun onClick()
}

class Circle(val r: Double) : Shape(), Clickable {
    override val area = Math.PI * r * r
    override fun onClick() { }
}

# 2.6.5 综合案例与思考

多接口冲突消歧义完整案例:

interface Runner {
    fun start() { println("Runner start") }
}
interface Loggable {
    fun start() { println("Log start") }
    fun log(msg: String) { println("[LOG] $msg") }
}

class Job : Runner, Loggable {
    override fun start() {
        super<Runner>.start()
        super<Loggable>.start()
        log("job started")
    }
}

fun main() { Job().start() }

输出:

Runner start
Log start
[LOG] job started

思考:Kotlin 为什么不允许多继承类,却允许实现多个带默认方法的接口?

原因:菱形继承问题——两个父类如果有相同名字的字段/方法,子类无法确定用哪个(Java 也是这个原因禁止多继承)。接口没有字段(只有派生属性),所以不存在「状态菱形」;方法冲突用 super<A>.foo() 显式消歧义即可,语义清晰。


# 2.7 委托机制

# 2.7.1 类委托by

interface Repo { fun save(): Int }
class RepoImpl : Repo { override fun save() = 1 }

class LoggingRepo(val delegate: Repo) : Repo by delegate {
    override fun save(): Int {
        println("before save")
        return delegate.save()
    }
}

fun main() {
    val repo: Repo = LoggingRepo(RepoImpl())
    repo.save()
}

Repo by delegate 表示「接口 Repo 的方法全部转发到 delegate」,可以选择性 override 加逻辑。组合优于继承 的语言级支持。

编译产物——class LoggingRepo(val delegate: Repo) : Repo by delegate 大致等价于:

class LoggingRepo(val delegate: Repo) : Repo {
    override fun save() = delegate.save()   // 自动生成
    // 若 Repo 有 fetch()/delete()/...,通通生成转发
}

Kotlin 编译器帮你把 100 行 boilerplate 缩成一个 by。

# 2.7.2 属性委托by

val lazyName: String by lazy { computeName() }
var observed: String by Delegates.observable("init") { _, old, new ->
    println("$old -> $new")
}

属性委托规则:val prop: T by delegate 要求 delegate 有 getValue(...) 方法;var prop: T by delegate 还要求有 setValue(...) 方法。签名:

operator fun getValue(thisRef: Any?, property: KProperty<*>): T
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T)

自己写一个属性委托:

class UpperCase {
    private var value = ""
    operator fun getValue(thisRef: Any?, prop: KProperty<*>): String = value
    operator fun setValue(thisRef: Any?, prop: KProperty<*>, v: String) {
        value = v.uppercase()
    }
}

class Person {
    var name: String by UpperCase()
}

fun main() {
    val p = Person()
    p.name = "kotlin"
    println(p.name)         // 输出: KOTLIN
}

# 2.7.3 内建委托家族

委托 用途
lazy 惰性求值
Delegates.observable 属性变化观察
Delegates.vetoable 属性变化否决(返回 false 拒绝赋值)
Delegates.notNull 无初始值的非空 val 变量
Map 委托 val name: String by map 从 Map 里取值

vetoable 示例:

var age: Int by Delegates.vetoable(0) { _, _, new -> new in 0..150 }
age = 30       // ✅ 生效
age = -5       // ❌ 被拒绝,仍是 30

Map 委托示例——常用于反序列化:

class Config(map: Map<String, Any?>) {
    val name: String by map
    val port: Int by map
}

val cfg = Config(mapOf("name" to "server", "port" to 8080))
println("${cfg.name}:${cfg.port}")        // server:8080

委托字节码原理详见原理篇 5。

# 2.7.4 综合案例与思考

给 Repository 加日志与缓存两层装饰,全部用委托:

interface UserRepo {
    fun find(id: Long): User?
    fun save(u: User)
}

class UserRepoDb : UserRepo {
    override fun find(id: Long) = User(id, "db-$id")
    override fun save(u: User) = println("db save $u")
}

// 装饰一:日志
class LoggingUserRepo(inner: UserRepo) : UserRepo by inner {
    override fun find(id: Long): User? {
        println("[LOG] find $id")
        return (this as UserRepo).let { inner ->    // 巧妙:直接调 delegate
            // 简化写法:委托方法可以直接被 super 或者绕过——我们改写 override
            null  // 举例简化
        }
    }
}

// 装饰二:缓存
class CachingUserRepo(private val inner: UserRepo) : UserRepo by inner {
    private val cache = mutableMapOf<Long, User>()
    override fun find(id: Long): User? = cache.getOrPut(id) {
        inner.find(id) ?: return null
    }
}

fun main() {
    val repo: UserRepo = CachingUserRepo(LoggingUserRepo(UserRepoDb()))
    repo.find(1)
    repo.find(1)        // 第二次命中缓存
}

思考:类委托与 Java 的动态代理(Proxy.newProxyInstance)区别在哪?

区别对比:

维度 Kotlin by 委托 Java Proxy 动态代理
生成时机 编译期生成字节码 运行期反射生成
性能 直接方法调用,等同于普通调用 慢约 5~10 倍
灵活性 编译期决定 运行期决定,可拦截任何方法
典型用例 组合优于继承的语法糖 AOP、拦截器、Mock 框架

# 2.8 泛型与型变

# 2.8.1 泛型类与函数

class Box<T>(val value: T)                              // 泛型类
fun <T> singletonList(item: T): List<T> = listOf(item)  // 泛型函数

Kotlin 泛型采用 类型擦除 模型——运行期没有 T 的具体类型信息(与 Java 一致)。想在运行期看到类型,用 reified(2.8.3)。

泛型约束:

fun <T : Number> sum(list: List<T>): Double = list.sumOf { it.toDouble() }

<T : Number> 表示 T 必须是 Number 或其子类。

# 2.8.2 in与out型变

关键字 含义 记忆
out T 协变(生产者) 只 出 不进
in T 逆变(消费者) 只 进 不出
(无) 不变 两者都可
interface Producer<out T> { fun produce(): T }        // 只返回 T
interface Consumer<in T>  { fun consume(t: T) }        // 只消费 T

// 协变:Producer<String> 可赋值给 Producer<Any>
val p1: Producer<String> = ...
val p2: Producer<Any> = p1              // ✅

// 逆变:Consumer<Any> 可赋值给 Consumer<String>
val c1: Consumer<Any> = ...
val c2: Consumer<String> = c1           // ✅

记忆技巧——「PECS」原则(Producer Extends,Consumer Super):

  • 类只 返回 T——协变(out)
  • 类只 消费 T——逆变(in)
  • 类既返回又消费——不变

为什么 List<String> 不能直接赋值给 List<Any>?——因为 List 在 Kotlin 里是可读的(get)也 是可写的(虽然 MutableList 才有 add)——如果允许赋值,你就可以:

val strs: MutableList<String> = mutableListOf("a")
val anys: MutableList<Any> = strs           // 假设允许
anys.add(42)                                // 往 List<Any> 塞了 Int
val s: String = strs[1]                     // 💥 ClassCastException

所以 Kotlin 只让 只读的 List 协变(interface List<out E>),可变的 MutableList 不变。

# 2.8.3 reified具化类型

Kotlin 独有——在 inline 函数里 能保留泛型类型:

inline fun <reified T> Any.isType(): Boolean = this is T

val ok = "hi".isType<String>()   // true
val no = 42.isType<String>()     // false

用途:解析 JSON、反射、类型检查等场景,避免手动传 Class<T>:

inline fun <reified T> Json.decode(json: String): T = 
    parse(json, T::class.java)     // ✅ 直接拿到 T::class

// 调用侧
val user = json.decode<User>(str)

字节码原理见 原理篇 4。

# 2.8.4 where约束语法

多重约束用 where:

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<T>
    where T : CharSequence, T : Comparable<T> {
    return list.filter { it > threshold }
}

fun main() {
    val r = copyWhenGreater(listOf("apple", "banana", "cherry"), "b")
    println(r)                      // [banana, cherry]
}

where T : CharSequence, T : Comparable<T> 表示 T 必须同时满足两个约束——「是字符序列」并且「可比较」。

# 2.8.5 综合案例与思考

泛型 EventBus 设计:

import kotlin.reflect.KClass

object EventBus {
    private val subs = mutableMapOf<KClass<*>, MutableList<(Any) -> Unit>>()

    inline fun <reified T : Any> subscribe(noinline handler: (T) -> Unit) {
        @Suppress("UNCHECKED_CAST")
        subs.getOrPut(T::class) { mutableListOf() }
            .add(handler as (Any) -> Unit)
    }

    fun publish(event: Any) {
        subs[event::class]?.forEach { it(event) }
    }
}

data class LoginEvent(val userId: Long)
data class LogoutEvent(val userId: Long)

fun main() {
    EventBus.subscribe<LoginEvent> { println("登录: ${it.userId}") }
    EventBus.subscribe<LogoutEvent> { println("登出: ${it.userId}") }
    EventBus.publish(LoginEvent(100))
    EventBus.publish(LogoutEvent(100))
}

输出:

登录: 100
登出: 100

reified 让 subscribe<LoginEvent> { } 这种调用可以拿到 LoginEvent::class——不用像 Java EventBus 那样传 LoginEvent.class 参数。

思考:List<out Any> 和 List<Any> 差在哪?为什么 List<String> 不能直接赋值给 List<Any>?

答案:

  • List<Any> 是不变的——只接受 List<Any> 类型。
  • List<out Any> 是使用点协变——接受 List<T> 其中 T <: Any(即任何 List)。
  • List<String> 不能直接赋给 List<Any> 的原因见 2.8.2 的反例分析;但可以赋给 List<out Any>——因为 out 标注保证了只读,不会破坏类型安全。

# 2.9 综合案例练习

# 2.9.1 电商订单系统

需求:

  • 订单有 4 种状态:待支付 / 已支付 / 已发货 / 已完成
  • 状态转换有严格规则
  • 订单详情懒加载
  • 每次状态变化打日志
import kotlin.properties.Delegates

sealed class OrderState {
    object Pending : OrderState()
    object Paid : OrderState()
    object Shipped : OrderState()
    object Done : OrderState()
}

data class OrderDetail(val items: List<String>, val amount: Long)

class Order(val id: Long, private val loader: () -> OrderDetail) {

    // 详情懒加载——首次访问才计算
    val detail: OrderDetail by lazy { loader() }

    // 状态变化自动打日志
    var status: OrderState by Delegates.observable<OrderState>(OrderState.Pending) { _, old, new ->
        println("Order#$id: ${old::class.simpleName} → ${new::class.simpleName}")
    }

    fun pay() {
        require(status is OrderState.Pending) { "只能对待支付订单调用 pay()" }
        status = OrderState.Paid
    }

    fun ship() {
        require(status is OrderState.Paid) { "只能对已支付订单调用 ship()" }
        status = OrderState.Shipped
    }

    fun complete() {
        require(status is OrderState.Shipped) { "只能对已发货订单调用 complete()" }
        status = OrderState.Done
    }
}

fun main() {
    val order = Order(1001L) {
        println("[db] 加载订单详情...")
        OrderDetail(listOf("iPhone", "Case"), 8999)
    }
    order.pay()
    order.ship()
    // 此时才触发 detail 加载
    println("金额: ${order.detail.amount}")
    order.complete()
}

输出:

Order#1001: Pending → Paid
Order#1001: Paid → Shipped
[db] 加载订单详情...
金额: 8999
Order#1001: Shipped → Done

关键点:

  1. sealed class OrderState + data class 组合建模
  2. by lazy 让详情按需加载
  3. Delegates.observable 让状态变化自动打日志
  4. require 强制状态转换规则

# 2.9.2 状态机建模

自动售货机——用 sealed class + when 做穷尽转换:

sealed class VendingState
object Idle : VendingState()
data class Inserted(val amount: Int) : VendingState()
data class Selected(val amount: Int, val item: String) : VendingState()
data class Dispensing(val item: String, val change: Int) : VendingState()
object Ready : VendingState()

sealed class Event
data class Insert(val coin: Int) : Event()
data class Select(val item: String, val price: Int) : Event()
object Take : Event()

fun transition(state: VendingState, event: Event): VendingState = when (state) {
    Idle -> when (event) {
        is Insert -> Inserted(event.coin)
        else -> state
    }
    is Inserted -> when (event) {
        is Insert -> Inserted(state.amount + event.coin)
        is Select -> if (state.amount >= event.price)
                        Selected(state.amount, event.item) else state
        else -> state
    }
    is Selected -> Dispensing(state.item, state.amount - state.item.length)  // 简化
    is Dispensing -> Ready
    Ready -> Idle
}

fun main() {
    var s: VendingState = Idle
    listOf(Insert(5), Insert(3), Select("Coke", 6), Take, Take).forEach {
        s = transition(s, it)
        println("→ $s")
    }
}

输出(简化):

→ Inserted(amount=5)
→ Inserted(amount=8)
→ Selected(amount=8, item=Coke)
→ Dispensing(item=Coke, change=4)
→ Ready

编译器要求 when 穷尽——如果新增一个状态(如「找零」),所有 when 分支都会编译报错。


# 2.10 类和对象底层原理

# 2.10.1 属性编译成什么

一段 Kotlin:

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

用 kotlinc -d out/ 编译后,javap -p out/Person.class:

public final class Person {
    private final java.lang.String name;
    private int age;
    public Person(java.lang.String, int);
    public final java.lang.String getName();
    public final int getAge();
    public final void setAge(int);
}

逐字段分析:

Kotlin 声明 生成 Java 字段 生成 getter 生成 setter
val name: String private final String name getName() 无(val)
var age: Int private int age getAge() setAge(int)

@NotNull 参数校验注入——对非空引用类型(如 String),编译器在构造入口注入 Intrinsics.checkNotNullParameter(...):

0: aload_1
1: ldc           #14   // String name
3: invokestatic  Intrinsics.checkNotNullParameter
6: aload_0
7: aload_1
8: putfield      Person.name

如果 Java 侧调用 new Person(null, 10),会立即抛 IllegalArgumentException——这就是 Kotlin 空安全从 Kotlin 世界向 Java 世界的边界防御。

# 2.10.2 data类生成物

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

public final class User {
    private final long id;
    private final String name;
    public User(long, String);
    public final long getId();
    public final String getName();
    public final long component1();        // 用于解构
    public final String component2();
    public final User copy(long, String);  // copy(id = ..., name = ...)
    public static User copy$default(User, long, String, int, Object);
    public String toString();              // "User(id=..., name=...)"
    public int hashCode();
    public boolean equals(Object);
}

每个方法的实现骨架(伪代码):

public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User)) return false;
    User that = (User) o;
    return this.id == that.id && this.name.equals(that.name);
}

public int hashCode() {
    return Long.hashCode(id) * 31 + name.hashCode();
}

详见 原理篇 2。

# 2.10.3 object的字节码

object AppConfig { } 生成:

public final class AppConfig {
    public static final AppConfig INSTANCE;
    private AppConfig() { }
    static {                              // 静态初始化块
        INSTANCE = new AppConfig();
    }
}

静态初始化时序:JVM 保证同一个 class 只被类加载器加载 一次,加载时进入 <clinit> 静态初始化块——INSTANCE 在这个阶段被赋值。JVM 类加载本身持有 lock,天然线程安全。

Kotlin 单例 vs Java 传统单例对比:

实现方式 代码量 线程安全 性能
Java 双检锁 15 行 ✅(正确写才安全) 每次 get 有 volatile 开销
Java 静态内部类 10 行 ✅ 无开销
Kotlin object 1 行 ✅(类加载器保证) 无开销

# 2.10.4 委托的字节码

class LoggingRepo(val delegate: Repo) : Repo by delegate 生成:

public final class LoggingRepo implements Repo {
    private final Repo delegate;

    public LoggingRepo(Repo delegate) {
        this.delegate = delegate;
    }
    public Repo getDelegate() { return delegate; }

    // 编译器自动生成的转发方法
    public int save() {
        return delegate.save();
    }
    // Repo 里所有方法都会生成一个类似的转发
}

性能对比——委托是编译期生成、直接方法调用,与 Java Proxy 相比:

维度 Kotlin by Java Proxy
每次调用开销 一次 invokevirtual invocationHandler.invoke + 反射
微基准(1 亿次) 约 50 ms 约 500 ms
类字节码 编译期生成 运行期动态生成

# 2.11 训练题三道

训练题 1(Builder 模式):用 apply 或 DSL 语法为 HttpRequest 写一个链式 Builder,要求最终产物是 data class。

参考答案
data class HttpRequest(
    val url: String,
    val method: String = "GET",
    val headers: Map<String, String> = emptyMap(),
    val body: String? = null,
)

class HttpRequestBuilder {
    var url: String = ""
    var method: String = "GET"
    private val headers = mutableMapOf<String, String>()
    var body: String? = null

    fun header(name: String, value: String) {
        headers[name] = value
    }

    fun build() = HttpRequest(url, method, headers.toMap(), body)
}

fun httpRequest(block: HttpRequestBuilder.() -> Unit): HttpRequest =
    HttpRequestBuilder().apply(block).build()

fun main() {
    val r = httpRequest {
        url = "https://api.demo.com/users"
        method = "POST"
        header("Content-Type", "application/json")
        body = "{\"name\":\"kotlin\"}"
    }
    println(r)
}

陷阱点:如果不用 headers.toMap() 复制,外部拿到 HttpRequest.headers 后修改会影响 Builder 内部的 map——data class 就失去了不可变承诺。

训练题 2(状态机):用 sealed class 建模一台自动售货机(空闲、投币、选货、出货、找零 5 状态),并写 transition(event: Event): State 函数,保证 when 分支穷尽。

参考答案
sealed class State {
    object Idle : State()
    data class Coined(val cash: Int) : State()
    data class Selected(val cash: Int, val item: Item) : State()
    data class Dispensing(val item: Item) : State()
    data class Changing(val remain: Int) : State()
}

data class Item(val name: String, val price: Int)

sealed class Event {
    data class Coin(val amount: Int) : Event()
    data class Select(val item: Item) : Event()
    object Cancel : Event()
}

fun transition(s: State, e: Event): State = when (s) {
    State.Idle -> when (e) {
        is Event.Coin -> State.Coined(e.amount)
        else -> s
    }
    is State.Coined -> when (e) {
        is Event.Coin -> State.Coined(s.cash + e.amount)
        is Event.Select ->
            if (s.cash >= e.item.price) State.Selected(s.cash, e.item) else s
        Event.Cancel -> State.Changing(s.cash)
    }
    is State.Selected -> State.Dispensing(s.item)
    is State.Dispensing -> State.Changing(0)   // 简化
    is State.Changing -> State.Idle
}

陷阱点:when 分支必须穷尽 State 的所有子类,任何遗漏都会编译报错——这是 sealed class 相对 open class 的核心优势。

训练题 3(观察者模式):用属性委托 Delegates.observable 实现一个 LiveData<T> 简化版,支持多观察者、取消订阅。

参考答案
import kotlin.properties.Delegates

class LiveData<T>(initial: T) {
    private val observers = mutableListOf<(T) -> Unit>()

    var value: T by Delegates.observable(initial) { _, _, new ->
        observers.forEach { it(new) }
    }

    fun observe(cb: (T) -> Unit): () -> Unit {
        observers += cb
        return { observers -= cb }        // 返回「取消订阅」函数
    }
}

fun main() {
    val ld = LiveData(0)
    val unsub1 = ld.observe { println("A: $it") }
    val unsub2 = ld.observe { println("B: $it") }
    ld.value = 1
    ld.value = 2
    unsub1()
    ld.value = 3           // 只剩 B
}

输出:

A: 1
B: 1
A: 2
B: 2
B: 3

陷阱点:observers.forEach 期间如果 callback 内 也 调用 observe / unsub——会 ConcurrentModificationException。生产环境要用 observers.toList().forEach { } 或者更严谨的读写分离。


# 2.12 新手陷阱Top5

# 陷阱 症状 正确姿势
1 data class 继承 继承 data class 破坏等价对称性 data class 永远设为 final
2 object 与 companion object 混淆 想要静态方法但访问不到 类级用 companion,全局用 object
3 lateinit 未初始化访问 UninitializedPropertyAccessException ::prop.isInitialized 先判断
4 构造顺序踩坑 主构造属性未初始化时 init 已运行 别在 init 依赖后声明的属性
5 open 忘加导致无法 override 子类 override 编译报错 有意为之的可继承点显式 open

扩展陷阱 4 项(进阶开发者也常踩):

  • 陷阱 6:companion object 里的常量没加 const,Java 侧调用要走 Companion.INSTANCE.getFoo()——很啰嗦。凡是编译期常量(基础类型/字符串)都加 const val。
  • 陷阱 7:data class 的 copy 是 浅复制——如果内部有 MutableList 字段,两个实例共享同一个 list。需要深复制得手动 copy(list = list.toMutableList())。
  • 陷阱 8:sealed class 的子类如果是普通 class(不是 object 或 data class),when 里的 is 匹配后 不会 自动 smart cast 到子类型的具体属性——需要 is XXX -> ... 明确指定。
  • 陷阱 9:enum 的 values() 每次调用都返回 新数组——高频调用要缓存 val ALL = values() 或用 Kotlin 1.9 新增的 entries(返回不可变 List)。

下一章预告:03.高阶特性入门 会带你尝一尝 Kotlin 独有的味道——扩展函数、DSL、协程初识、Java 互操作。

上次更新: 2026/07/05, 15:10:54
Kotlin基础语法
高阶特性入门

← Kotlin基础语法 高阶特性入门→

最近更新
01
11.数据库的原理总结
07-12
02
11.操作系统原理总结
07-10
03
资深程序员软能力
07-10
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式