主题
第 6 章 类与对象(class / data class / object)
学习目标:会定义类、属性、构造函数;理解
data class自动生成了什么;掌握object单例;区分companion object和 Javastatic;会用密封类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 不同:
- 不用
new关键字- 主构造函数写在类名后
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) // 调 getter6.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 个主构造参数
- 主构造参数必须是
val或var - 不能是
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_LENGTH6.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 个:
equals()—— 基于所有主构造属性hashCode()—— 同上toString()——ClassName(prop1=value1, prop2=value2)copy(prop1 = ..., prop2 = ...)—— 拷贝改字段componentN()—— 给解构声明用,按属性顺序component1,component2, ...
Q3. data class 能继承别的类吗?能被继承吗?
答:
- 可以继承别的类(接口、抽象类、open class)
- 不能被继承(编译器自动加
final),因为equals等方法的实现假设没子类
如果实在需要继承体系,用普通 class + 手动写 equals/hashCode。
Q4. companion object 跟 Java 的 static 一样吗?
答:不完全一样。
- 字节码层面:
companion object是真实的对象(Companion内部类的实例),调用走对象方法 - 语法上看起来像
static:User.create()看着像User.create() - 可以实现接口、被继承、做泛型,比 Java static 灵活
- 想让 Java 端真的当 static 用,要加
@JvmStatic注解
Q5. lateinit 和 by lazy 区别?
答:
| 维度 | lateinit var | by 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) // trueQ9. 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)) }
}