Kotlin常见类

92 阅读10分钟

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)

编译器自动生成的方法

  1. equals() / hashCode()
  2. toString()
  3. copy()
  4. componentN()(用于解构)

2. sealed class(密封类)

用途
限制继承层次的类,只能在同一个文件中被继承,常与 when 表达式结合实现类型安全的分支

特点

  • 默认是 abstract,不能直接实例化。
  • 继承类必须在同一文件中定义。
  • 常用于有限状态建模(状态机、网络请求状态等)。
  • 可以是普通类、data classobject 的组合。

示例

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)自动生成 equalshashCodecopytoString数据封装
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 classenum class 的区别?

  • 答案

    • enum class固定、离散且等价的枚举值,每个枚举项是单例;适合状态只有常量名、少量属性或简单分支。
    • sealed class:限定继承层次(同一文件内定义所有子类),每个分支可以是不同形态的类型(含 data class/object/普通类),可携带不同结构的数据,更灵活,适合状态建模
  • 使用指引

    • “只有名字”的有限集合 → enum
    • “有限但异构数据/行为”的集合 → sealed class / sealed interface
  • when 可穷尽性sealed 分支可被编译器校验“是否穷尽”,enum 也可(所有枚举项)。


4) data classequals() 是结构相等还是引用相等?

  • 答案结构相等(基于 主构造参数中的 val/ var 属性值)。
  • 补充:引用相等用 === 检查。
  • 易错点:把 var 放到数据类里会导致对象放入 Hash 容器后修改属性→hashCode 变化→集合行为异常。最佳实践:数据类属性尽量 val 不可变

5) 为什么 data class 中建议用 val 而不是 var

  • 答案

    • 不可变更安全:避免哈希容器中对象被修改导致 hashCode 失配;
    • 语义清晰:体现值对象不可变(Value Object)。
  • 实践建议:业务上确需更改,考虑复制+替换copy())而非原位修改。


6) sealed classabstract class 区别?

  • 答案

    • sealed class限制继承范围(子类需在同一文件声明),常用于穷尽分支;默认 abstract,不能直接实例化。
    • abstract class:仅规定不能实例化,可有抽象/非抽象成员,不限制子类数量/位置。
  • 场景

    • “我需要一个有限且可穷尽的分支集合”→ sealed
    • “我需要一个带部分实现的可继承父类”→ abstract

7) sealed interfacesealed class 的区别?

  • 答案

    • sealed interface:限制实现者的范围(同一文件),但更偏契约;实现可以是类/对象/数据类等。
    • sealed class:限制子类范围,并可提供共享实现/状态。
  • 怎么选:若仅需“有限集合的类型契约”,选 sealed interface;若还需共享状态/实现,选 sealed class


8) data classcopy() 是浅拷贝还是深拷贝?

  • 答案浅拷贝
  • 后果:若属性中包含可变对象(如 MutableList),copy() 后两者会共享底层引用。需要深拷贝时手写或封装扩展方法。

9) sealed class 在多模块项目中如何扩展?

  • 答案:Kotlin 的 sealed 要求所有直接子类必须在同一个源文件内声明(与父类同文件)。这意味着运行时不可在别的模块新增分支

  • 工程实践

    • 把所有分支集中到一个 sealed 定义文件;
    • 若确需跨模块扩展,sealed 不合适,考虑开放层次结构open/abstract/接口),用运行时注册/策略模式等替代。

10) 如何让 data class 的某个属性不参与 equals() / hashCode()

  • 答案:只能不把它放进主构造参数(改为常规属性或次构造/init 中初始化),或改用普通类自定义 equals/hashCode
  • 替代:将不参与比较的字段放到 @Transientlateinit 或者放到 val meta: Any? 外部容器,但要清楚语义。

二、更多高频/进阶面试题(附简洁要点)

A. data class 与普通 class 的区别?

  • 编译器自动生成:equals/hashCode/toString/copy/componentN
  • 更适合值对象、DTO。普通类需手写;数据类默认 final

B. whensealed 的穷尽性校验

  • when(x: Sealed) 覆盖了所有子类型分支即可省略 else
  • 若新增分支,编译器会提示未覆盖,从而避免漏判(这是 sealed 的关键价值)。

C. 数据类解构(componentN)生成规则

  • 仅对主构造参数中的属性生成;顺序为参数声明顺序;
  • 次构造/类体属性参与;属性太多时虽可生成,但可读性变差,不推荐过度解构。

D. equals 对可空/集合的影响

  • 数据类里属性是集合(尤其可变集合)时要谨慎:

    • 变更后 hashCode 改变 → HashMap/HashSet 失效;
    • 建议使用不可变视图或 val + 新实例替换。

E. objectcompanion objectdata 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 classcopy/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.Errelse
  • 面试点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 更“轻”。

四、加分题(经常把人卡住)

  1. data class 的继承
  • 数据类默认 final,若要继承需 open,但数据类几乎不推荐被继承(语义不稳定、生成方法易混乱)。更合理:用组合封闭层次sealed)。
  1. 伴生对象 vs “静态”
  • Kotlin 没有 static,用 companion object 暴露类级别成员;Java 侧会看到一个静态字段持有伴生对象实例。
  1. when 与智能类型转换
  • is 分支里自动 smart cast;但涉及可空或多线程可变引用时,可能失效,需要局部 val 缓存或显式转换。
  1. 跨模块的“可扩展枚举”
  • 想让别人加分支?sealed 不适用,考虑开放接口 + 服务发现(SPI)/注册表模式。
  1. 算子型数据类(表达式树/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:单属性、可消除装箱、轻量类型安全包装。