跳到主要内容

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 的限制
  1. 主构造函数必须至少有一个参数
  2. 参数必须标记为 valvar
  3. 不能是 abstractopensealedinner
  4. 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 classabstract 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 的区别?

答案

  1. companion object 是一个真实的对象,可以实现接口、有自己的名字
  2. 编译后不是真正的 Java static,而是通过 INSTANCE 访问
  3. 需要 @JvmStatic 注解才能从 Java 侧作为静态方法调用
class Factory {
companion object : Serializable { // 可以实现接口!
@JvmStatic
fun create() = Factory()
}
}

Q5: Kotlin 为什么类默认是 final 的?

答案

这是 Effective Java 第 19 条的实践:"设计并文档化继承,否则禁止继承"。

原因:

  1. 继承容易破坏封装 — 子类可能依赖父类的实现细节
  2. 默认 final 鼓励组合 — Kotlin 哲学是 "组合优于继承"
  3. 性能 — final 方法可以被 JVM 内联优化
  4. 安全 — 防止意外的子类化行为

需要继承时显式标记 open,这是一种有意识的设计决定。

相关链接