Skip to content

第 6 章 类与对象(class / data class / object)

学习目标:会定义类、属性、构造函数;理解 data class 自动生成了什么;掌握 object 单例;区分 companion object 和 Java static;会用密封类 sealed class 做类型安全的分支建模。


6.1 最简单的类

kotlin
class User                                    // 空类,连大括号都不用
class Point(val x: Int, val y: Int)           // 主构造函数 + 两个 val 属性

val u = User()                                // 实例化(不需要 new)
val p = Point(1, 2)
println(p.x)                                  // 1

📌 跟 Java 不同

  1. 不用 new 关键字
  2. 主构造函数写在类名后
  3. val x: Int 在构造里同时声明了"属性 + 构造参数"

6.1.1 类默认是 final

kotlin
class Animal     // 不能被继承

open class Animal    // 加 open 才能继承
class Dog : Animal()

💡 为啥默认 final?继承是 90% 的 bug 来源。Kotlin 强制你显式声明"这个类我设计来给你继承"。


6.2 主构造函数与初始化块

6.2.1 主构造函数

kotlin
class User(val name: String, val age: Int)
// ↑ 主构造函数:声明在类名后

// 等价于:
class User {
    val name: String
    val age: Int
    
    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

6.2.2 init 块

主构造函数没有方法体,要做初始化逻辑用 init 块:

kotlin
class User(val name: String, val age: Int) {
    init {
        require(age >= 0) { "年龄不能为负数" }
        println("创建了 User $name")
    }
}

可以有多个 init 块,按出现顺序执行。

6.2.3 次构造函数

kotlin
class User(val name: String, val age: Int) {
    constructor(name: String) : this(name, 0)        // 必须委托给主构造
    constructor() : this("Unknown", 0)
}

📌 Kotlin 推荐:用默认参数代替次构造函数。

kotlin
// 推荐
class User(val name: String = "Unknown", val age: Int = 0)

6.3 属性与字段

6.3.1 属性 = 字段 + getter + setter

kotlin
class User(var name: String, val age: Int) {
    // val 属性:只有 getter
    // var 属性:getter + setter
}

val u = User("Alice", 25)
u.name = "Bob"        // 调 setter
println(u.name)       // 调 getter

6.3.2 自定义 getter / setter

kotlin
class Rectangle(val width: Int, val height: Int) {
    val area: Int                        // 计算属性,不存字段
        get() = width * height
}

class User(name: String) {
    var name: String = name
        set(value) {
            field = value.trim()         // field 是后台字段
            println("name 被改成: $field")
        }
}

📌 field 关键字:在 setter 里访问"后台字段",避免 setter 内部递归调用自己。

6.3.3 lateinit:延迟初始化

kotlin
class Service {
    lateinit var dependency: Dependency
    
    fun init() {
        dependency = createDependency()
    }
    
    fun use() {
        dependency.foo()                // 用之前必须初始化,否则抛 UninitializedPropertyAccessException
    }
}

lateinit 限制:

  • 必须 var(不能 val)
  • 必须是非空类型
  • 不能是基本类型(Int / Long 等)—— 用 Int?0 by Delegates.notNull()

6.3.4 by lazy:第一次用时才初始化

kotlin
class Service {
    val database: Database by lazy {
        println("初始化数据库...")
        Database.connect()
    }
}

val s = Service()
println("Service 创建完成")
s.database                              // 这一刻才会初始化
s.database                              // 第二次直接用缓存

💡 场景:初始化代价高、不一定会用到的资源(DB 连接、配置加载、缓存)。


6.4 data class —— Kotlin 最爱

6.4.1 一行顶 50 行

kotlin
data class User(val id: String, val name: String, val age: Int)

编译器自动生成:

  • equals() —— 基于所有属性比较
  • hashCode() —— 基于所有属性
  • toString() —— User(id=u001, name=Alice, age=25)
  • copy() —— 复制对象,可改部分字段
  • componentN() —— 解构声明用

6.4.2 实战用法

kotlin
val u1 = User("u001", "Alice", 25)
val u2 = User("u001", "Alice", 25)

println(u1 == u2)                       // true(结构相等)
println(u1)                              // User(id=u001, name=Alice, age=25)

// copy:常用于不可变更新
val u3 = u1.copy(age = 26)              // 只改年龄

// 解构声明
val (id, name, age) = u1
println("$id, $name, $age")

6.4.3 data class 的限制

  • 必须有至少 1 个主构造参数
  • 主构造参数必须是 valvar
  • 不能是 abstract / open / sealed / inner
  • 主构造里的属性才参与 equals / hashCode / toString / componentN

6.5 继承

6.5.1 基本语法

kotlin
open class Animal(val name: String) {
    open fun sound() = "..."
}

class Dog(name: String) : Animal(name) {
    override fun sound() = "Woof!"
}

class Cat(name: String) : Animal(name) {
    override fun sound() = "Meow!"
}

📌 关键字

  • open —— "我允许被继承 / 重写"
  • override —— "我重写了父类的方法"(必须显式写,编译器才放过)
  • final override —— "重写后不允许再被重写"

6.5.2 抽象类

kotlin
abstract class Shape {
    abstract fun area(): Double           // 必须实现
    fun describe() = "面积 = ${area()}"   // 默认实现,子类可不重写
}

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

6.5.3 接口

kotlin
interface Drawable {
    fun draw()                            // 必须实现
    fun describe() = "我是一个 Drawable"  // 默认实现(Java 8+ 也支持)
}

interface Named {
    val name: String                      // 接口可以有抽象属性
}

class Circle(override val name: String) : Drawable, Named {
    override fun draw() = println("画一个圆 $name")
}

6.6 object 关键字

6.6.1 object 单例

kotlin
object DatabaseManager {
    fun connect() = println("连接数据库")
    fun disconnect() = println("断开数据库")
}

DatabaseManager.connect()                 // 直接用,不需要 new

📌 类比 Java:等价于 Java 的 "private 构造 + static instance" 单例模式,但 Kotlin 一行搞定。

6.6.2 companion object 伴生对象

Kotlin 没有 static,但有 companion object

kotlin
class User(val name: String) {
    companion object {
        const val MAX_NAME_LENGTH = 50
        
        fun create(name: String): User {
            require(name.length <= MAX_NAME_LENGTH)
            return User(name)
        }
    }
}

User.create("Alice")                     // 看起来像调"静态工厂方法"
User.MAX_NAME_LENGTH

6.6.3 object 表达式(替代匿名内部类)

kotlin
val listener = object : ClickListener {
    override fun onClick() = println("clicked")
}

📌 类比 Java 的 new ClickListener() { ... } 匿名内部类。


6.7 enum 与 sealed class:类型安全的分支建模

6.7.1 enum class

kotlin
enum class Status(val code: Int) {
    PENDING(0),
    ACTIVE(1),
    DELETED(2);
    
    fun isLive() = this != DELETED
}

println(Status.ACTIVE.code)
println(Status.PENDING.isLive())

6.7.2 sealed class —— 比 enum 更强大

kotlin
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Failure(val error: Throwable) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

fun handle(r: Result<String>) = when (r) {
    is Result.Success -> println("成功: ${r.data}")
    is Result.Failure -> println("失败: ${r.error}")
    Result.Loading -> println("加载中...")
    // 不需要 else!编译器知道所有情况都覆盖了
}

📌 sealed 的威力:所有子类必须在同一个文件 / 模块里,编译器知道完整列表。when 覆盖所有分支后不需要 else

生活化类比

  • enum 像「预制菜」—— 固定几种,每种都一个样
  • sealed class 像「拼盘菜」—— 几种固定类型,但每种里面可以装不同内容(带数据的)

6.8 实战:购物车数据建模

kotlin
data class Product(val id: String, val name: String, val price: Double)

data class CartItem(val product: Product, val quantity: Int) {
    val subtotal: Double get() = product.price * quantity
}

class Cart {
    private val items = mutableListOf<CartItem>()
    
    fun add(product: Product, quantity: Int = 1) {
        items.add(CartItem(product, quantity))
    }
    
    val total: Double get() = items.sumOf { it.subtotal }
    val itemCount: Int get() = items.sumOf { it.quantity }
    
    fun summary() = """
        购物车小计:
        - 商品种类: ${items.size}
        - 总件数:   $itemCount
        - 总金额:   ¥${"%.2f".format(total)}
    """.trimIndent()
}

fun main() {
    val cart = Cart()
    cart.add(Product("p001", "iPhone", 8999.0))
    cart.add(Product("p002", "AirPods", 1299.0), quantity = 2)
    println(cart.summary())
}

6.9 章末小结

              ★ 第 6 章核心知识图谱 ★

        ┌──────────────┼──────────────┐
        │              │              │
     ┌──▼──┐       ┌──▼──┐        ┌──▼──┐
     │ 类  │       │data │        │object│
     ├─────┤       ├─────┤        ├─────┤
     │主构造│       │自动 │        │单例  │
     │init │       │equals│       │compa-│
     │open │       │copy │        │nion  │
     │覆盖 │       │结构 │        │object│
     │继承 │       │化   │        │      │
     └─────┘       └─────┘        └─────┘

                  ┌────▼────┐
                  │sealed   │
                  ├─────────┤
                  │类型安全 │
                  │分支建模 │
                  │when 穷尽│
                  └─────────┘

🎤 6.10 章末面试题(10 道)

Q1. Kotlin 类为啥默认 final?怎么继承?

:避免"为继承而设计"的复杂性。Java 默认所有类都可继承,但绝大部分类作者根本没想过被继承(导致脆弱)。Kotlin 强制要在 open class A 才能 class B : A()

成员函数同理:默认 final,要 open 才能被 override。

💡 这是 Joshua Bloch 在《Effective Java》里的建议:"Design and document for inheritance or else prohibit it."


Q2. data class 自动生成了哪些方法?

:5 个:

  1. equals() —— 基于所有主构造属性
  2. hashCode() —— 同上
  3. toString() —— ClassName(prop1=value1, prop2=value2)
  4. copy(prop1 = ..., prop2 = ...) —— 拷贝改字段
  5. componentN() —— 给解构声明用,按属性顺序 component1, component2, ...

Q3. data class 能继承别的类吗?能被继承吗?

  • 可以继承别的类(接口、抽象类、open class)
  • 不能被继承(编译器自动加 final),因为 equals 等方法的实现假设没子类

如果实在需要继承体系,用普通 class + 手动写 equals/hashCode。


Q4. companion object 跟 Java 的 static 一样吗?

不完全一样

  • 字节码层面:companion object真实的对象Companion 内部类的实例),调用走对象方法
  • 语法上看起来像 staticUser.create() 看着像 User.create()
  • 可以实现接口、被继承、做泛型,比 Java static 灵活
  • 想让 Java 端真的当 static 用,要加 @JvmStatic 注解

Q5. lateinitby lazy 区别?

维度lateinit varby lazy { }
可变性var(可重赋值)val(只能一次)
是否非空必须非空可空可非空
基本类型不能(Int / Long 等)可以
初始化时机你手动赋值第一次访问时自动
线程安全不保证默认 SYNCHRONIZED 模式线程安全
典型场景DI 注入、Android findViewById单例资源、配置缓存

Q6. object 单例是怎么保证线程安全的?

:编译器把它编译成 Java 风格的"静态初始化器":

java
// Kotlin: object Foo { fun bar() = ... }
// 编译后等价于:
public final class Foo {
    public static final Foo INSTANCE = new Foo();
    private Foo() {}
    public final void bar() { ... }
}

JVM 保证类加载是线程安全的(class loader 用 monitor 锁),所以 INSTANCE 的初始化天然单例 + 线程安全。


Q7. sealed class 比 enum 强在哪?

  • enum:每个值是同类型的实例,只能携带固定字段
  • sealed class:每个子类可以是 不同结构(不同字段、不同函数),更适合"和类型"(sum type)建模
kotlin
// 用 enum 做不出"成功带数据 / 失败带异常 / 加载中"
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Failure(val error: Throwable) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

when 表达式对 sealed class 自动检查穷尽性,不需要 else


Q8. equals / hashCode 在 Kotlin 里用 ===== 怎么区分?

  • a == b:调用 a.equals(b)结构相等
  • a === b引用相等(同一个对象)
kotlin
val u1 = User("Alice", 25)
val u2 = User("Alice", 25)
println(u1 == u2)         // true(data class 自动 equals)
println(u1 === u2)        // false(不是同一个对象)

val u3 = u1
println(u1 === u3)        // true

Q9. Kotlin 类和接口在多继承时怎么处理冲突?

:Kotlin 单继承类 + 多实现接口(同 Java)。如果两个接口的同名方法冲突,子类必须显式重写

kotlin
interface A { fun foo() = "A" }
interface B { fun foo() = "B" }

class C : A, B {
    override fun foo() = super<A>.foo() + super<B>.foo()
    // 用 super<父类型>.方法() 指定调哪个
}

Q10. data class 拷贝时改字段,原对象会变吗?

不会copy() 创建一个新对象,原对象不受影响。

kotlin
val u1 = User("Alice", 25)
val u2 = u1.copy(age = 26)
println(u1)   // User(name=Alice, age=25)  ← 没变
println(u2)   // User(name=Alice, age=26)  ← 新对象
println(u1 === u2)   // false

这是函数式编程"不可变更新"的核心思想:每次"修改"都返回一个新对象,原对象保持不变。在并发、状态管理(如 Redux)里特别重要。


下一章 → 第 7 章 · 集合 & 函数式操作 →

🎬 可视化演示

演示加载缓慢或样式异常?点此在新标签页打开 ↗

💻 示例代码

kotlin
/**
 * 第 6 章 · 类与对象 — 综合示例
 *
 * 涵盖:data class / 主构造 + init / 计算属性 / lazy /
 *      继承 + override / object 单例 / companion object /
 *      sealed class / 解构声明
 */

// ========== 1. data class ==========
data class User(val id: String, val name: String, val age: Int)

// ========== 2. 普通 class + 主构造 + init + 计算属性 ==========
class Rectangle(val width: Double, val height: Double) {
    init {
        require(width > 0 && height > 0) { "宽高必须为正数" }
    }
    val area: Double get() = width * height
    val perimeter: Double get() = 2 * (width + height)
    override fun toString() = "Rect(${width}x${height}, area=$area)"
}

// ========== 3. lateinit + by lazy ==========
class Service {
    lateinit var dependency: String
    val database: String by lazy {
        println("  ⏳ 第一次访问,初始化数据库...")
        "DB-Connection-${System.currentTimeMillis()}"
    }
}

// ========== 4. 继承 ==========
open class Animal(val name: String) {
    open fun sound() = "..."
    fun introduce() = "我是 $name,${sound()}"
}
class Dog(name: String) : Animal(name) {
    override fun sound() = "汪汪!"
}
class Cat(name: String) : Animal(name) {
    override fun sound() = "喵~"
}

// ========== 5. 接口 + 多实现 ==========
interface Drawable { fun draw(): String }
interface Named { val name: String }

class Circle(override val name: String, val r: Double) : Drawable, Named {
    override fun draw() = "画一个圆 $name (r=$r)"
}

// ========== 6. object 单例 ==========
object DatabaseManager {
    private var connectionCount = 0
    fun connect(): String {
        connectionCount++
        return "Connection #$connectionCount"
    }
    fun stats() = "总连接数: $connectionCount"
}

// ========== 7. companion object ==========
class HttpClient private constructor(val baseUrl: String) {
    companion object {
        const val DEFAULT_TIMEOUT = 5000
        fun create(baseUrl: String) = HttpClient(baseUrl)
    }
}

// ========== 8. sealed class ==========
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Failure(val error: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

fun handle(r: Result<String>): String = when (r) {
    is Result.Success -> "✅ 成功: ${r.data}"
    is Result.Failure -> "❌ 失败: ${r.error}"
    Result.Loading -> "⏳ 加载中..."
}

fun main() {
    println("=== 1. data class ===")
    val u1 = User("u001", "Alice", 25)
    val u2 = User("u001", "Alice", 25)
    val u3 = u1.copy(age = 26)
    println("u1 = $u1")
    println("u1 == u2 ? ${u1 == u2}")    // true
    println("u1 === u2 ? ${u1 === u2}")  // false
    println("u3 = $u3")
    val (id, name, age) = u1
    println("解构: id=$id, name=$name, age=$age")

    println("\n=== 2. 普通类 + 计算属性 ===")
    val rect = Rectangle(3.0, 4.0)
    println(rect)
    println("周长 = ${rect.perimeter}")
    try {
        Rectangle(-1.0, 2.0)
    } catch (e: IllegalArgumentException) {
        println("⚠️ 校验失败: ${e.message}")
    }

    println("\n=== 3. lateinit 和 lazy ===")
    val s = Service()
    s.dependency = "MyDep"
    println("dependency = ${s.dependency}")
    println("准备访问 database... (会触发懒初始化)")
    println("database = ${s.database}")
    println("再次访问 database (用缓存)")
    println("database = ${s.database}")

    println("\n=== 4. 继承 ===")
    val animals: List<Animal> = listOf(Dog("旺财"), Cat("咪咪"), Animal("X"))
    animals.forEach { println(it.introduce()) }

    println("\n=== 5. 接口 + 多实现 ===")
    val c = Circle("c1", 5.0)
    println(c.draw())
    println("name (from Named) = ${c.name}")

    println("\n=== 6. object 单例 ===")
    DatabaseManager.connect()
    DatabaseManager.connect()
    DatabaseManager.connect()
    println(DatabaseManager.stats())

    println("\n=== 7. companion object ===")
    val client = HttpClient.create("https://api.example.com")
    println("客户端: ${client.baseUrl}")
    println("默认超时: ${HttpClient.DEFAULT_TIMEOUT}ms")

    println("\n=== 8. sealed class + when ===")
    listOf<Result<String>>(
        Result.Success("OK"),
        Result.Failure("网络错误"),
        Result.Loading,
    ).forEach { println(handle(it)) }
}

ClassDemo.kt ↗