阅读完需:约 14 分钟
密封类
密封(Sealed
)类是一个限制类层次结构的类。 可以在类名之前使用sealed
关键字将类声明为密封类。 它用于表示受限制的类层次结构。
当对象具有来自有限集的类型之一,但不能具有任何其他类型时,使用密封类。
密封类的构造函数在默认情况下是私有的,它也不能允许声明为非私有。
在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。

密封类定义



sealed class MyClass
密封类的子类必须在密封类的同一文件中声明。
sealed class Shape{
class Circle(var radius: Float): Shape()
class Square(var length: Int): Shape()
class Rectangle(var length: Int, var breadth: Int): Shape()
object NotAShape : Shape()
}
密封类通过仅在编译时限制类型集来确保类型安全的重要性。
sealed class A{
class B : A()
{
class E : A() //this works.
}
class C : A()
init {
println("sealed class A")
}
}
class D : A() // this works
{
class F: A() // 不起作用,因为密封类在另一个范围内定义。
}
密封类隐式是一个无法实例化的抽象类。
sealed class MyClass
fun main(args: Array<String>)
{
var myClass = MyClass() // 编译器错误,密封类型无法实例化。
}
总结:其实就是一个只能在同一个文件中定义子类的抽象类。定义方式是在类的前面加sealed
关键字
例子:
控制播放器播放状态的例子
data class Song(val name: String, val url: String, var position: Int)
data class ErrorInfo(val code: Int, val message: String)
object Songs {
val StarSky = Song("Star Sky", "https://fakeurl.com/321144.mp3", 0)
}
sealed class PlayerState
object Idle : PlayerState()
class Playing(val song: Song) : PlayerState() {
fun start() {}
fun stop() {}
}
class Error(val errorInfo: ErrorInfo) : PlayerState() {
fun recover() {}
}
class Player {
var state: PlayerState = Idle
fun play(song: Song) {
this.state = when (val state = this.state) {
Idle -> {
Playing(song).also(Playing::start)
}
is Playing -> {
state.stop()
Playing(song).also(Playing::start)
}
is Error -> {
state.recover()
Playing(song).also(Playing::start)
}
}
}
}
fun main() {
val player = Player()
player.play(Songs.StarSky)
}
密封类和 when 的使用
密封类通常与表达时一起使用。 由于密封类的子类将自身类型作为一种情况。 因此,密封类中的when
表达式涵盖所有情况,从而避免使用else
子句。
既然使用了 sealed
密封类,那么 when
中就必定要涵盖所有的情况,不然就会报错,如果不使用 sealed
密封类 就必须要加上 else
,否则会遗漏其他的情况。

例子:
sealed class Shape{
class Circle(var radius: Float): Shape()
class Square(var length: Int): Shape()
class Rectangle(var length: Int, var breadth: Int): Shape()
// object NotAShape : Shape()
}
fun eval(e: Shape) =
when (e) {
is Shape.Circle ->println("Circle area is ${3.14*e.radius*e.radius}")
is Shape.Square ->println("Square area is ${e.length*e.length}")
is Shape.Rectangle ->println("Rectagle area is ${e.length*e.breadth}")
//else -> "else case is not require as all case is covered above"
// Shape.NotAShape ->Double.NaN
}
fun main(args: Array<String>) {
var circle = Shape.Circle(5.0f)
var square = Shape.Square(5)
var rectangle = Shape.Rectangle(4,5)
eval(circle)
eval(square)
eval(rectangle)
}
密封与枚举对比

枚举和抽象类的局限性
先来看一下枚举的局限性:
- 限制枚举每个类型只允许有一个实例
- 限制所有枚举常量使用相同的类型的值
限制枚举每个类型只允许有一个实例
enum class Color(val value: Int) {
Red(1)
}
fun main(args: Array<String>) {
val red1 = Color.Red
val red2 = Color.Red
println("${red1 == red2}") // true
}
red1 == red2 : true
正如你看到的,我们定义了一个单元素的枚举类型,无论 Color.Red 有多少个对象,最终他们的实例都是一个,每个枚举常量仅作为一个实例存在,而一个密封类的子类可以有多个包含状态的实例,这既是枚举的局限性也是枚举的优点。
枚举常量作为一个实例存在的优点: 枚举不仅能防止多次实例化,而且还可以防止反序列化,还能避免多线程同步问题,所以它也被列为实现单例方法之一。简单汇总一下。
是否只有一个实例 | 是否反序列化 | 是否是线程安全 | 是否是懒加载 |
---|---|---|---|
是 | 是 | 是 | 否 |
《Effective Java》 一书的作者 Josh Bloch 建议我们使用枚举作为单例,虽然使用枚举实现单例的方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton 的最佳方法。
我们来看一下如何用枚举实现一个单例(与 Java 的实现方式相同)
interface ISingleton {
fun doSomething()
}
enum class Singleton : ISingleton {
INSTANCE {
override fun doSomething() {
// to do
}
};
fun getInstance(): Singleton = Singleton.INSTANCE
}
但是在实际项目中使用枚举作为单例的很少,我看了很多开源项目,将枚举作为单例的场景少之有少,很大部分原因是因为使用枚举的时候非常不方便。
我这有个建议如果涉及反序列化创建对象的时候,建议使用枚举,因为 Java 规定,枚举的序列化和反序列化是有特殊定制的,因此禁用编译器使用 writeObject
、readObject
、readObjectNoData
、 writeReplace
、readResolve
等方法。
限制所有枚举常量使用相同的类型的值
限制所有枚举常量使用相同的类型的值,也就是说每个枚举常量类型的值是相同的,我们还是用刚才的例子做个演示。
enum class Color(val value: Int) {
Red(1),
Green(2),
Blue(3);
}
正如你所见,我们在枚举 Color 中定义了三个常量 Red 、Green 、Blue,但是它们只能使用 Int 类型的值,不能使用其他类型的值,如果使用其它类型的值会怎么样?如下所示:

编译器会告诉你只接受 Int 类型的值,无法更改它的类型,也就是说你无法为枚举类型,添加额外的信息。
抽象类的局限性
对于一个抽象类我们可以用一些子类去继承它,但是子类不是固定的,它可以随意扩展,同时也失去枚举常量的受限性。
Sealed Classes 包含了抽象类和枚举的优势:抽象类表示的灵活性和枚举常量的受限性
到这里可能会有一个疑问,如果 Sealed Classes 没有枚举和抽象类的局限性,那么它能在实际项目中给我们带来哪些好处呢?在了解它能带来哪些好处之前,我们先来看看官方对 Sealed Classes 的解释。
Sealed Classes
- Sealed Classes 用于表示受限制的类层次结构
- 从某种意义上说,Sealed Classes 是枚举类的扩展
- 枚举的不同之处在于,每个枚举常量仅作为单个实例存在,而 Sealed Classes 的子类可以表示不同状态的实例
Sealed Classes 用于表示受限制的类层次结构
Sealed Classes 用于表示受限制的类层次结构,其实这句话可以拆成两句话来理解。
- Sealed Classes 用于表示层级关系: 子类可以是任意的类, 数据类、Kotlin 对象、普通的类,甚至也可以是另一个 Sealed
- Sealed Classes 受限制: 必须在同一文件中,或者在 Sealed Classes 类的内部中使用,在 Kotlin 1.1 之前,规则更加严格,子类只能在 Sealed Classes 类的内部中使用
Sealed Classes 的用法也非常的简单,我们来看一下如何使用 Sealed Classes。
sealed class Color {
class Red(val value: Int) : Color()
class Green(val value: Int) : Color()
class Blue(val name: String) : Color()
}
fun isInstance(color: Color) {
when (color) {
is Color.Red -> TODO()
is Color.Green -> TODO()
is Color.Blue -> TODO()
}
}
从某种意义上说,Sealed Classes 是枚举类的扩展,其实 Sealed Classes 和枚举很像,我们先来看一个例子。

正如你所看到的,在 Sealed Classes 内部中,使用 object 声明时,我们可以重用它们,不需要每次创建一个新实例,当这样使用时候,它看起来和枚举非常相似。
注意:实际上很少有人会这么使用,而且也不建议这么用,因为在这种情况枚举比 Sealed Classes 更适合
在什么情况下使用枚举或者 Sealed?
- 如果涉及反序列化创建对象的时候,建议使用枚举,因为 Java 规定,枚举的序列化和反序列化是有特殊定制的,因此禁用编译器使用
writeObject
、readObject
、readObjectNoData
、writeReplace
、readResolve
等方法。 - 如果你不需要多次实例化,也不需要不提供特殊行为,或者也不需要添加额外的信息,仅作为单个实例存在,这个时候使用枚举更加合适。
- 其他情况下使用 Sealed Classes,在一定程度上可以使用 Sealed Classes 代替枚举
密封类的实例:递归整型列表的简单实现
sealed class IntList {
object Nil: IntList() {
override fun toString(): String {
return "Nil"
}
}
data class Cons(val head: Int, val tail: IntList): IntList(){
override fun toString(): String {
return "$head, $tail"
}
}
fun joinToString(sep: Char = ','): String {
return when(this){
Nil -> "Nil"
is Cons -> {
"${head}$sep${tail.joinToString(sep)}"
}
}
}
}
fun IntList.sum(): Int {
return when(this){
IntList.Nil -> 0
is IntList.Cons -> head + tail.sum()
}
}
operator fun IntList.component1(): Int? {
return when(this){
IntList.Nil -> null
is IntList.Cons -> head
}
}
operator fun IntList.component2(): Int? {
return when(this){
IntList.Nil -> null
is IntList.Cons -> tail.component1()
}
}
operator fun IntList.component3(): Int? {
return when(this){
IntList.Nil -> null
is IntList.Cons -> tail.component2()
}
}
fun intListOf(vararg ints: Int): IntList {
return when(ints.size){
0 -> IntList.Nil
else -> {
IntList.Cons(
ints[0],
//array前面加* 展开数组
intListOf(*(ints.slice(1 until ints.size).toIntArray()))
)
}
}
}
// [0, 1, 2, 3]
fun main() {
//val list = IntList.Cons(0, IntList.Cons(1, IntList.Cons(2, IntList.Cons(3, IntList.Nil))))
val list = intListOf(0, 1, 2, 3)
println(list)
println(list.joinToString('-'))
println(list.sum())
val (first, second, third) = list
println(first)
println(second)
println(third)
//val (a, b, c, d, e) = listOf<Int>()
}
Kotlin 中的密封类 优于 带标签的类
我们主要从类层次结构来讨论一下 Sealed Classes(密封类) 和 Tagged Classes(标记类)的优缺点。在开始分析之前,我们先介绍一下什么是 Tagged Classes(标记类)以及都有那些缺点。
Tagged Classes 是什么
在一个类中包含一个指示操作的标记字段或者特征,方便在它们之间切换的类称为 Tagged Classes(标记类)
class Figure(
// 这个标签字段:用来表示图形的形状
val shape: Shape,
// 这个字段用于圆形
val radius: Double = 0.0,
// 这两个字段用于矩形
val length: Double = 0.0,
val width: Double = 0.0
) {
// 定义了两个形状 矩形、圆形
enum class Shape {
RECTANGLE, CIRCLE
}
// 计算当前图形的面积
fun area(): Double = when (shape) {
Shape.RECTANGLE -> length * width
Shape.CIRCLE -> Math.PI * (radius * radius)
else -> throw AssertionError(shape)
}
companion object {
fun createRectangle(radius: Double) {
Figure(
shape = Shape.RECTANGLE,
radius = radius
)
}
fun createCircle(length: Double, width: Double = 0.0) {
Figure(
shape = Shape.CIRCLE,
length = length,
width = width
)
}
}
}
正如你所见,代码中包含了很多模板代码,包括标记字段、切换语句、枚举等等,在一个类中包含了很多不同的操作,如果以后增加新的操作,有需要增加新的标记,实际情况这样的代码在项目中非常的常见,主要存在以下几个问题:
- 增加了很多模板代码
- 内存是非常稀缺的资源,当我们创建圆形的时候,与它无关的字段也要保留,增加当前类所占用的内存
- 降低了代码的可读性,类中混合了很多操作例如枚举、切换语句等等,为了保证对象正确的创建,通常需要用到工厂模式等等设计模式
- 如果增加新的图形,不得不去修改原有的代码结构
- ……
那么有没有很好的替换方案,可以解决以上所有的问题,而且还可以在不修改原有的代码结构基础上增加新的图形,这就需要用到类的层次结构。
类的层次结构
无论是 Java 还是 Kotlin 我们都会使用类的层次结构代替标记类,而在 Kotlin 中我们常用 Sealed Classes 表示受限制的类层次结构
sealed class Figure {
abstract fun area(): Double
class Rectangle(val length: Double, val width: Double) : Figure() {
override fun area(): Double = length * width
}
class Circle(val radius: Double) : Figure() {
override fun area(): Double = Math.PI * (radius * radius)
}
}
正如你所见,代码简洁干净了很多,不包含模板代码,并且类之间的职责分明,提高了代码的灵活性,完美的解决了上述所有的缺点。每个类中不包含无关的字段,同时在类中添加新的参数,并不会影响其他类。
如果我们需要增加新的图形,只需要新增加一个类即可,并不会破坏原有的代码结构,例如这里我们增加一个球形。
class Ball(val radius: Double) : Figure() {
override fun area(): Double = 4.0 * Math.PI * Math.pow(radius, 2.0)
}
不仅仅如此,Sealed Classes 结合 when 表达式一起使用会更加的方便
fun Figure.Valida() {
when (this) {
is Figure.Ball -> {
println("I am Ball")
area()
}
is Figure.Circle -> {
println("I am Circle")
area()
}
is Figure.Rectangle -> {
println("I am Rectangle")
area()
}
}
}