1. data class(数据类)
用途:
专门用来存储数据的类,编译器会自动生成一些常用方法。
特点:
- 必须有至少一个主构造参数。
- 主构造参数默认会被用作
equals()、hashCode()、toString()、copy()、componentN()等方法的生成依据。 - 默认是 final(不能被继承),除非显式使用
open。 - 常用于 DTO(Data Transfer Object)、VO(Value Object)等。
示例:
kotlin
复制编辑
data class User(val id: Int, val name: String)
val u1 = User(1, "Tom")
val u2 = u1.copy(name = "Jerry")
println(u1) // User(id=1, name=Tom)
编译器自动生成的方法:
equals()/hashCode()toString()copy()componentN()(用于解构)
2. sealed class(密封类)
用途:
限制继承层次的类,只能在同一个文件中被继承,常与 when 表达式结合实现类型安全的分支。
特点:
- 默认是
abstract,不能直接实例化。 - 继承类必须在同一文件中定义。
- 常用于有限状态建模(状态机、网络请求状态等)。
- 可以是普通类、
data class、object的组合。
示例:
kotlin
复制编辑
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val exception: Throwable) : Result()
object Loading : Result()
}
fun handle(result: Result) = when (result) {
is Result.Success -> println("成功: ${result.data}")
is Result.Error -> println("错误: ${result.exception}")
Result.Loading -> println("加载中")
}
3. open class(可继承类)
特点:
- Kotlin 默认类是
final,要允许继承必须加open。 - 适用于需要被多个子类继承的普通父类。
kotlin
复制编辑
open class Animal {
open fun sound() = println("Animal sound")
}
class Dog : Animal() {
override fun sound() = println("Bark")
}
4. abstract class(抽象类)
特点:
- 不能直接实例化。
- 可以包含抽象方法(无方法体)和非抽象方法。
- 常用于模板方法模式(Template Method)等需要部分实现的父类。
kotlin
复制编辑
abstract class Shape {
abstract fun draw()
fun printInfo() = println("This is a shape")
}
5. enum class(枚举类)
特点:
- 表示一组固定常量。
- 枚举项本质是单例对象。
- 可以实现接口、定义属性和方法。
kotlin
复制编辑
enum class Direction(val degrees: Int) {
NORTH(0), EAST(90), SOUTH(180), WEST(270);
}
6. 常见区别总结表
| 类类型 | 是否可继承 | 特殊功能 | 典型用途 |
|---|---|---|---|
data class | ❌(final) | 自动生成 equals、hashCode、copy、toString | 数据封装 |
sealed class | 同文件内 | 限制继承范围,配合 when | 状态建模 |
open class | ✅ | 可被继承 | 普通父类 |
abstract class | ✅ | 抽象方法,部分实现 | 模板父类 |
enum class | ❌(final) | 枚举常量唯一性 | 固定常量 |
一、逐题详解(含要点 + 反问点)
1) Kotlin 中类默认是 final 还是 open?
- 答案:默认是
final。 - 延伸:若希望被继承/重写,需显式标记
open(或使用abstract/sealed等隐含可继承语义)。 - 易错点:方法/属性同理,想让子类重写需
open,子类用override。
2) data class 为什么需要至少一个主构造参数?
-
答案:编译器需要至少一个主构造参数来生成
equals()/hashCode()/toString()/copy()/componentN()等“数据”语义。 -
补充:主构造参数要用
val/var才会参与上述生成逻辑(仅写普通参数不会记录为属性,也不会参与比较)。 -
易错点:
- 写成
data class User(id: Int)(少了val/var)→id不是属性,不参与比较/解构。 - 次构造函数里的属性 不参与
componentN()。
- 写成
3) sealed class 与 enum class 的区别?
-
答案:
enum class:固定、离散且等价的枚举值,每个枚举项是单例;适合状态只有常量名、少量属性或简单分支。sealed class:限定继承层次(同一文件内定义所有子类),每个分支可以是不同形态的类型(含data class/object/普通类),可携带不同结构的数据,更灵活,适合状态建模。
-
使用指引:
- “只有名字”的有限集合 →
enum; - “有限但异构数据/行为”的集合 →
sealed class / sealed interface。
- “只有名字”的有限集合 →
-
when 可穷尽性:
sealed分支可被编译器校验“是否穷尽”,enum也可(所有枚举项)。
4) data class 的 equals() 是结构相等还是引用相等?
- 答案:结构相等(基于 主构造参数中的
val/var属性值)。 - 补充:引用相等用
===检查。 - 易错点:把
var放到数据类里会导致对象放入 Hash 容器后修改属性→hashCode变化→集合行为异常。最佳实践:数据类属性尽量val不可变。
5) 为什么 data class 中建议用 val 而不是 var?
-
答案:
- 不可变更安全:避免哈希容器中对象被修改导致
hashCode失配; - 语义清晰:体现值对象不可变(Value Object)。
- 不可变更安全:避免哈希容器中对象被修改导致
-
实践建议:业务上确需更改,考虑复制+替换(
copy())而非原位修改。
6) sealed class 和 abstract class 区别?
-
答案:
sealed class:限制继承范围(子类需在同一文件声明),常用于穷尽分支;默认abstract,不能直接实例化。abstract class:仅规定不能实例化,可有抽象/非抽象成员,不限制子类数量/位置。
-
场景:
- “我需要一个有限且可穷尽的分支集合”→
sealed; - “我需要一个带部分实现的可继承父类”→
abstract。
- “我需要一个有限且可穷尽的分支集合”→
7) sealed interface 与 sealed class 的区别?
-
答案:
sealed interface:限制实现者的范围(同一文件),但更偏契约;实现可以是类/对象/数据类等。sealed class:限制子类范围,并可提供共享实现/状态。
-
怎么选:若仅需“有限集合的类型契约”,选
sealed interface;若还需共享状态/实现,选sealed class。
8) data class 的 copy() 是浅拷贝还是深拷贝?
- 答案:浅拷贝。
- 后果:若属性中包含可变对象(如
MutableList),copy()后两者会共享底层引用。需要深拷贝时手写或封装扩展方法。
9) sealed class 在多模块项目中如何扩展?
-
答案:Kotlin 的
sealed要求所有直接子类必须在同一个源文件内声明(与父类同文件)。这意味着运行时不可在别的模块新增分支。 -
工程实践:
- 把所有分支集中到一个
sealed定义文件; - 若确需跨模块扩展,
sealed不合适,考虑开放层次结构(open/abstract/接口),用运行时注册/策略模式等替代。
- 把所有分支集中到一个
10) 如何让 data class 的某个属性不参与 equals() / hashCode()?
- 答案:只能不把它放进主构造参数(改为常规属性或次构造/
init中初始化),或改用普通类自定义equals/hashCode。 - 替代:将不参与比较的字段放到
@Transient、lateinit或者放到val meta: Any?外部容器,但要清楚语义。
二、更多高频/进阶面试题(附简洁要点)
A. data class 与普通 class 的区别?
- 编译器自动生成:
equals/hashCode/toString/copy/componentN; - 更适合值对象、DTO。普通类需手写;数据类默认
final。
B. when 与 sealed 的穷尽性校验
when(x: Sealed)覆盖了所有子类型分支即可省略else;- 若新增分支,编译器会提示未覆盖,从而避免漏判(这是
sealed的关键价值)。
C. 数据类解构(componentN)生成规则
- 仅对主构造参数中的属性生成;顺序为参数声明顺序;
- 次构造/类体属性不参与;属性太多时虽可生成,但可读性变差,不推荐过度解构。
D. equals 对可空/集合的影响
-
数据类里属性是集合(尤其可变集合)时要谨慎:
- 变更后
hashCode改变 → HashMap/HashSet 失效; - 建议使用不可变视图或
val+ 新实例替换。
- 变更后
E. object、companion object、data object
object:线程安全的单例。companion object:与类关联的单例(相当于 Java 的静态成员容器)。data object:单例 + 更友好的toString/equals语义(表示唯一值对象的单例常量),适合sealed分支里的“无数据态”。
F. enum vs object vs sealed
enum:固定常量集合(同构、常量名最重要),可附加少量状态;sealed + data class/object:固定但异构的状态集(每个分支形态可不同);object:单例,若只是“一个值”,且需要穷尽校验/花样分支 → 放进sealed里当分支很合适。
G. value class(值类,旧称 inline class)与 data class 区别
value class:单属性、可在运行时被消除装箱(性能/内存更优),语义上接近“带类型的标量”;data class:多属性聚合对象,偏向业务数据建模;- 选择:标识符/轻量包装 →
value class;复合数据 →data class。
H. Java 互操作性注意
data class的copy/componentN在 Java 侧不直观;sealed在 Java 侧更熟悉的是 Java17+sealed,但 Kotlin 的规则不同(同文件);- 若公开 API 需 Java 友好度,考虑提供 builder/静态工厂方法。
I. sealed 层级深度与嵌套
- 可以嵌套:外层
sealed,内层各分支再sealed,但注意可读性; - 对
when而言,穷尽性校验仅对当前判定类型的直接分支有效。
J. copy() 的默认参数与防御式拷贝
copy(a = a.copy())这类递归式深拷贝要小心成本;- 对可变集合属性,在
copy内做防御式复制是常见实践。
三、带代码的小型问答题(实战风格)
1) 为什么这段 HashSet 行为异常?
kotlin
复制编辑
data class User(var id: Int, var name: String)
val set = hashSetOf(User(1,"Tom"))
val u = set.first()
u.name = "Jerry"
println(set.contains(User(1,"Jerry"))) // ?
- 解析:修改了参与
hashCode的字段(name),集合中的桶位置失配,contains可能返回false。 - 结论:数据类属性尽量
val;或不将可变值对象放入哈希容器。
2) sealed 分支漏判时编译器如何提示?
kotlin
复制编辑
sealed class Result {
data class Ok(val data: String): Result()
data class Err(val e: Throwable): Result()
}
fun handle(r: Result) = when (r) {
is Result.Ok -> println(r.data)
// 忘了 Err 分支
}
- 结果:编译不通过,要求
when穷尽;需补上is Result.Err或else。 - 面试点:
sealed的最大好处之一就是强制穷尽。
3) 如何给 sealed 分支加单例“无数据态”更优雅?
kotlin
复制编辑
sealed class LoadState {
data object Idle : LoadState()
data object Loading : LoadState()
data class Success(val data: List<Item>) : LoadState()
data class Error(val e: Throwable) : LoadState()
}
- 要点:
data object单例 + 友好toString;更贴合“唯一态值”。
4) 让某字段不参与比较
kotlin
复制编辑
data class Session(val id: String) {
// 不参与比较的临时字段
lateinit var cache: Cache
}
- 要点:把该字段移出主构造即可;或改普通类手写
equals/hashCode。
5) sealed interface 何时选?
kotlin
复制编辑
sealed interface UiEvent
data class Click(val id: Int): UiEvent
data object Back: UiEvent
- 要点:当你只需要受限的实现集合,并不需要共享实现/状态,用
sealed interface更“轻”。
四、加分题(经常把人卡住)
data class的继承
- 数据类默认
final,若要继承需open,但数据类几乎不推荐被继承(语义不稳定、生成方法易混乱)。更合理:用组合或封闭层次(sealed)。
- 伴生对象 vs “静态”
- Kotlin 没有
static,用companion object暴露类级别成员;Java 侧会看到一个静态字段持有伴生对象实例。
when与智能类型转换
is分支里自动 smart cast;但涉及可空或多线程可变引用时,可能失效,需要局部val缓存或显式转换。
- 跨模块的“可扩展枚举”
- 想让别人加分支?
sealed不适用,考虑开放接口 + 服务发现(SPI)/注册表模式。
- 算子型数据类(表达式树/AST)
- 用
sealed+data class表达加/乘/常量等分支,when做解释器,非常典型的函数式题。
五、速记对照(面试口述模板)
- data class:值对象;自动生成 equals/hashCode/toString/copy/componentN;主构造
val/var决定比较字段;浅拷贝;尽量不可变;不建议继承。 - sealed class/interface:受限继承 + when 穷尽;同文件内定义所有直接子类/实现;适合状态/事件建模;
data object很好用。 - abstract/open:结构开放;
abstract不能实例化,可部分实现;open允许继承/重写。 - enum:固定同构常量集;可携带少量参数/实现接口;不适合异构分支。
- value class:单属性、可消除装箱、轻量类型安全包装。