Kotlin 面向对象
问题
Kotlin 面向对象编程有哪些核心特性?data class、sealed class、object 各有什么用途?
答案
1. 类与构造函数
// 主构造函数 —— 在类头声明
class User(val name: String, var age: Int) {
// 初始化块
init {
require(age > 0) { "Age must be positive" }
}
// 次构造函数 —— 必须委托给主构造函数
constructor(name: String) : this(name, 0)
}
// 使用(Kotlin 没有 new 关键字)
val user = User("Alice", 25)
println(user.name) // 自动生成 getter
user.age = 26 // 自动生成 setter(var 属性)
2. data class
data class 自动生成 equals()、hashCode()、toString()、copy()、componentN() 方法:
data class User(
val name: String,
val age: Int,
val email: String = ""
)
val user1 = User("Alice", 25)
val user2 = User("Alice", 25)
println(user1 == user2) // true(自动生成 equals)
println(user1) // User(name=Alice, age=25, email=)
// copy —— 复制并修改部分属性(不可变数据的好伴侣)
val user3 = user1.copy(age = 26)
// 解构声明(基于 componentN)
val (name, age) = user1
data class 的限制
- 主构造函数必须至少有一个参数
- 参数必须标记为
val或var - 不能是
abstract、open、sealed或inner equals/hashCode只看主构造函数中的属性,类体中的属性不参与
3. sealed class / sealed interface
sealed class 是一种受限的类层级结构,所有子类在编译时已知,适合表示有限的状态集合:
// 密封类 —— 定义有限状态
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val cause: Exception? = null) : Result<Nothing>()
data object Loading : Result<Nothing>()
}
// 配合 when 使用 —— 编译器保证穷尽检查
fun handleResult(result: Result<String>) = when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
is Result.Loading -> println("Loading...")
// 不需要 else —— 编译器知道所有子类
}
sealed class vs enum
- enum:每个类型只有一个实例,不能携带不同的数据
- sealed class:每个子类可以有多个实例,可以携带不同的数据
- 当状态需要附带数据时,使用
sealed class
4. object 关键字
// 1. 对象声明 —— 单例模式
object DatabaseManager {
fun connect() { /* ... */ }
}
DatabaseManager.connect() // 直接使用
// 2. 伴生对象 —— 类级别的成员(类似 Java 的 static)
class MyClass {
companion object {
const val TAG = "MyClass"
fun create(): MyClass = MyClass()
}
}
MyClass.TAG // 访问常量
MyClass.create() // 调用工厂方法
// 3. 对象表达式 —— 匿名对象(类似 Java 匿名内部类)
val listener = object : View.OnClickListener {
override fun onClick(v: View?) {
// 处理点击
}
}
5. 继承与接口
// Kotlin 类默认是 final 的,需要 open 才能被继承
open class Animal(val name: String) {
open fun sound(): String = "..."
}
class Dog(name: String) : Animal(name) {
override fun sound(): String = "Woof!"
}
// 接口可以有默认实现
interface Clickable {
fun click()
fun showOff() = println("I'm clickable!") // 默认实现
}
// 多接口同名方法冲突时,必须显式选择
class Button : Clickable, Focusable {
override fun click() = println("Clicked!")
override fun showOff() {
super<Clickable>.showOff() // 选择 Clickable 的实现
}
}
6. 抽象类 vs 接口
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 构造函数 | ✅ 有 | ❌ 没有 |
| 状态(字段) | ✅ 可以有 | ❌ 不能有 backing field |
| 多继承 | ❌ 单继承 | ✅ 多实现 |
| 默认方法 | ✅ | ✅ |
| 访问修饰符 | 全部支持 | 不支持 protected |
7. 可见性修饰符
| 修饰符 | 类成员 | 顶层声明 |
|---|---|---|
public(默认) | 处处可见 | 处处可见 |
private | 类内可见 | 文件内可见 |
protected | 类内 + 子类可见 | 不适用 |
internal | 模块内可见 | 模块内可见 |
internal 修饰符
internal 是 Kotlin 独有的,表示同一模块内可见。在 Android 组件化开发中非常有用——可以对同模块暴露 API,但对其他模块隐藏实现细节。
编译后 internal 会被混淆为带 $ 的名称,从 Java 侧虽然可以调用但不推荐。
常见面试问题
Q1: data class 的 equals 和 hashCode 是怎么生成的?
答案:
data class 根据主构造函数中声明的属性自动生成 equals() 和 hashCode():
data class User(val name: String, val age: Int) {
var nickname: String = "" // ⚠️ 不参与 equals/hashCode
}
val u1 = User("Alice", 25).apply { nickname = "A" }
val u2 = User("Alice", 25).apply { nickname = "B" }
println(u1 == u2) // true —— nickname 不参与比较
这是常见的坑:类体中声明的属性不参与自动生成的方法。
Q2: sealed class 和 abstract class 的区别?
答案:
| 特性 | sealed class | abstract class |
|---|---|---|
| 子类范围 | 编译时已知(同模块) | 任何地方都可以继承 |
| when 穷尽检查 | ✅ 编译器保证 | ❌ 需要 else |
| 主要用途 | 状态建模、ADT | 抽象基类 |
sealed class 本质上是一个限制了子类范围的 abstract class,编译器知道所有可能的子类型,所以能做穷尽性检查。
Q3: object 声明是如何实现单例的?
答案:
Kotlin 的 object 声明编译后会生成一个线程安全的饿汉式单例:
// object AppConfig { val version = "1.0" }
// 编译后等价的 Java 代码:
public final class AppConfig {
public static final AppConfig INSTANCE;
private final String version = "1.0";
static {
INSTANCE = new AppConfig(); // 类加载时初始化
}
private AppConfig() {}
public final String getVersion() { return version; }
}
利用 JVM 的类加载机制保证线程安全,无需双重检查锁。
Q4: Kotlin 中 companion object 和 Java static 的区别?
答案:
companion object是一个真实的对象,可以实现接口、有自己的名字- 编译后不是真正的 Java
static,而是通过INSTANCE访问 - 需要
@JvmStatic注解才能从 Java 侧作为静态方法调用
class Factory {
companion object : Serializable { // 可以实现接口!
@JvmStatic
fun create() = Factory()
}
}
Q5: Kotlin 为什么类默认是 final 的?
答案:
这是 Effective Java 第 19 条的实践:"设计并文档化继承,否则禁止继承"。
原因:
- 继承容易破坏封装 — 子类可能依赖父类的实现细节
- 默认 final 鼓励组合 — Kotlin 哲学是 "组合优于继承"
- 性能 — final 方法可以被 JVM 内联优化
- 安全 — 防止意外的子类化行为
需要继承时显式标记 open,这是一种有意识的设计决定。