Kotlin Sealed 是什么?为什么 Google 都用

简介: 在上一篇文章 Google 推荐在项目中使用 Sealed 和 RemoteMediator 中介绍了如何使用 Sealed Classes 在 Flow 基础上对网络请求成功和失败进行处理,而这篇文章是对 Sealed Classes 更加深入的解析,结合函数式编程功能很强大,掌握并且灵活运用它,需要大量的实践。

image.png


在上一篇文章 Google 推荐在项目中使用 Sealed 和 RemoteMediator 中介绍了如何使用 Sealed Classes 在 Flow 基础上对网络请求成功和失败进行处理,而这篇文章是对 Sealed Classes 更加深入的解析,结合函数式编程功能很强大,掌握并且灵活运用它,需要大量的实践。


通过这篇文章你将学习到以下内容:


  • Sealed Classes 原理分析?
  • 枚举和抽象类都有那些局限性?
  • 为什么枚举可以作为单例?枚举作为单例有那些优点?
  • 分别在什么情况下使用枚举和 Sealed Classes?
  • Sealed Classes 究竟是什么?
  • 为什么 Sealed Classes 用于表示受限制的类层次结构?
  • 为什么说 Sealed Classes 是枚举类的扩展?
  • Sealed Classes 的子类可以表示不同状态的实例,那么在项目中如何使用?
  • 禁止在 Sealed Classes 所定义的文件外使用, Kotlin 是如何做到的呢?


枚举和抽象类的局限性



在分析 Sealed Classes 之前,我们先来分析一下枚举和抽象类都有那些局限性,注意:这些局限性是相对于 Sealed Classes 而言的,但是相对于它们自身而言是优点,而 Sealed Classes 出现也正是为了解决这些问题。先来看一下枚举的局限性:


  • 限制枚举每个类型只允许有一个实例
  • 限制所有枚举常量使用相同的类型的值


限制枚举每个类型只允许有一个实例


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 规定,枚举的序列化和反序列化是有特殊定制的,因此禁用编译器使用 writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve 等方法。


限制所有枚举常量使用相同的类型的值


限制所有枚举常量使用相同的类型的值,也就是说每个枚举常量类型的值是相同的,我们还是用刚才的例子做个演示。


enum class Color(val value: Int) {
    Red(1),
    Green(2),
    Blue(3);
}


正如你所见,我们在枚举 Color 中定义了三个常量 Red 、Green 、Blue,但是它们只能使用 Int 类型的值,不能使用其他类型的值,如果使用其它类型的值会怎么样?如下所示:


image.png


编译器会告诉你只接受 Int 类型的值,无法更改它的类型,也就是说你无法为枚举类型,添加额外的信息。


抽象类的局限性


对于一个抽象类我们可以用一些子类去继承它,但是子类不是固定的,它可以随意扩展,同时也失去枚举常量的受限性。


Sealed Classes 包含了抽象类和枚举的优势:抽象类表示的灵活性和枚举常量的受限性

到这里可能会有一个疑问,如果 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()
    }
}


在这里推荐大家一个快捷键 Mac/Win/Linux:Alt + Enter 可以补全 when 语句下的所有分支,效果如下所示:


image.png


更多 AndroidStudio 快捷键,可以看之前的两篇文章



Sealed Classes 是枚举类的扩展


从某种意义上说,Sealed Classes 是枚举类的扩展,其实 Sealed Classes 和枚举很像,我们先来看一个例子。


image.png


正如你所看到的,在 Sealed Classes 内部中,使用 object 声明时,我们可以重用它们,不需要每次创建一个新实例,当这样使用时候,它看起来和枚举非常相似。


注意:实际上很少有人会这么使用,而且也不建议这么用,因为在这种情况枚举比 Sealed Classes 更适合


在什么情况下使用枚举


如果你不需要多次实例化,也不需要不提供特殊行为,或者也不需要添加额外的信息,仅作为单个实例存在,这个时候使用枚举更加合适。


我们来看一下 Paging3 中是如何使用枚举的,一起来看一下 androidx.paging.LoadType 这个类的源码。


enum class LoadType {
    REFRESH,
    PREPEND,
    APPEND
}


枚举常量 作用
refresh 在初始化刷新的使用
append 在加载更多的时候使用
prepend 在当前列表头部添加数据的时候使用


它们不需要多次实例化,也不需要添加任何额外的信息,仅仅表示某种状态,而且它在很多地方都会用到比如 RemoteMediator、PagingSource 等等,想了解更多关于 Paging3 原理和实战案例可以看之前写的几篇文章。



4.Sealed Classes 的子类可以表示不同状态的实例


与枚举的不同之处在于,每个枚举常量仅作为单个实例存在,而 Sealed Classes 的子类可以表示不同状态的实例,我们来看个例子可能更容易理解这句话。


这里我们延用之前在 Google 推荐在项目中使用 sealed 和 RemoteMediator 这篇文章中用到的例子,在请求网络的时候需要对成功或者失败进行处理,我们来看一下用 Sealed Classes 如何进行封装。


sealed class PokemonResult<out T> {
    data class Success<out T>(val value: T) : PokemonResult<T>()
    data class Failure(val throwable: Throwable?) : PokemonResult<Nothing>()
}


这里只贴出来部分代码,核心实现可以查看项目 PokemonGoGitHub 地址:https://github.com/hi-dhl/PokemonGo代码路径:PokemonGo/app/.../com/hi/dhl/pokemon/data/remote/PokemonResult.kt


一起来看一下如何使用


when (result) {
    is PokemonResult.Failure -> {
        // 进行失败提示
    }
    is PokemonResult.Success -> {
        // 进行成功处理
    }
}


我们在来看另外一个例子,在一个列表中可能会有不同类型的数据,比如图片、文本等等,那么用 Sealed Classes 如何表示。


sealed class ListItem {
    class Text(val title: String, val content: String) : ListItem()
    class Image(val url: String) : ListItem()
}


这是两个比较常见的例子,当然 Sealed Classes 强大不止于此,还有更多场景,等着一起来挖掘。


我们来看一下大神 Antonio Leiva 在这篇文章 Sealed classes in Kotlin: enums with super-powers 分享的一个比较有趣的例子,对 View 进行的一系列操作可以封装在 Sealed Classes 中,我们来看一下会有什么样的效果。


sealed class UiOp {
    object Show: UiOp()
    object Hide: UiOp()
    class TranslateX(val px: Float): UiOp()
    class TranslateY(val px: Float): UiOp()
}
fun execute(view: View, op: UiOp) = when (op) {
    UiOp.Show -> view.visibility = View.VISIBLE
    UiOp.Hide -> view.visibility = View.GONE
    is UiOp.TranslateX -> view.translationX = op.px
    is UiOp.TranslateY -> view.translationY = op.px
}


在 Sealed Classes 类中,我们定义了一系列 View 的操作 ShowHideTranslateXTranslateY ,现在我们创建一个类,将这些对视图的操作整合在一起。


class Ui(val uiOps: List = emptyList()) {
    operator fun plus(uiOp: UiOp) = Ui(uiOps + uiOp)
}


在 Ui 这个类中声明了一个 List 存储了所有的操作,并重写了 plus 操作符,关于 plus 操作符可以看之前的文章 为数不多的人知道的 Kotlin 技巧以及 原理解析,通过 plus 操作符将这些对视图的操作拼接在一起,这样不仅可以提高代码的可读性,而且使用起来也非常的方便,都定义好之后,我们来看一下如何使用这个类。


val ui = Ui() +
        UiOp.Show +
        UiOp.TranslateX(20f) +
        UiOp.TranslateY(40f) +
        UiOp.Hide
run(view, ui)


定义了一系列操作之后,然后通过 run 方法来执行这些操作,来看一下 run 方法的实现。


fun run(view: View, ui: Ui) {
    ui.uiOps.forEach { execute(view, it) }
}


代码很简单,这里就不多做解释了,在 kotlin 中函数可以作为参数传递,可以将 run 方法传递给另一个函数或者一个类,并且这些操作完全可互换的,将它们结合在一起功能将非常强大。


Sealed Classes 强大不止于此,还有很多很多非常实用的场景,现在我对 Sealed Classes 的理解也非常有限,还不够灵活的使用它,我相信在更多项目,更多的场景,会看到更多实用的一些技巧。


Sealed Classes 原理



在这里我们还是使用上文中用到的例子,来分析 Sealed Classes 原理。


sealed class Color {
    object Red : Color()
    object Green : Color()
    object Blue : Color()
}


一起来分析一下反编译后的 Java 代码都做了什么。PS:Tools → Kotlin → Show Kotlin Bytecode


... // 省略部分代码
@Metadata(
   mv = {1, 1, 13},
   bv = {1, 0, 3},
   k = 1,
   d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0004\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\b6\u0018\u00002\u00020\u0001:\u0003\u0003\u0004\u0005B\u0007\b\u0002¢\u0006\u0002\u0010\u0002\u0082\u0001\u0003\u0006\u0007\b¨\u0006\t"},
   d2 = {"Lcom/hidhl/leetcode/test/saledvsemun/Color;", "", "()V", "Blue", "Green", "Red", "Lcom/hidhl/leetcode/test/saledvsemun/Color$Red;", "Lcom/hidhl/leetcode/test/saledvsemun/Color$Green;", "Lcom/hidhl/leetcode/test/saledvsemun/Color$Blue;", "Java-kotlin"}
)
public abstract class Color {
   private Color() {
   }
   public Color(DefaultConstructorMarker $constructor_marker) {
      this();
   }
}
... // 省略部分代码


@Metadata 这个注解会出现在 Kotlin 编译器生成的任何类文件中,可以通过反射的方式获取 @Metadata 信息。参数名称都非常短,可以帮助减少 class 文件的大小。


@Metadata 存储了 Kotlin 主要的语法信息例如扩展函数、typealias 等等,这些信息都是由 kotlinc 编译器,并以注解的形式存放在 Java 的字节码中的,如果元数据被丢弃掉,运行在 JVM 上会抛出异常,那么如何才能确定它们之间的对应关系呢,其实就是通过 @Metadata 这个注解提供的信息。


正因为元数据不能被丢掉,R8 带了新的优化,将元数据信息记录在 R8 的内部数据结构中,当 R8 完成对第三库或者应用程序的优化和收缩时,它会为所有 Kotlin 类合成新的正确的 Kotlin 元数据,其目的就是为了减少应用程序的大小,目前我也在研究中,日后会分享。


而在本例中  @Metadata 保存了一个子类的列表,编译器在使用的时候会用到这些信息。正如你看到的 Sealed class 被编译成了 abstract class,它本身是不能被实例化,只能用它的子类实例化对象。


抽象类 Color 默认的构造方法被私有化了,所以在 Kotlin 1.1 之前,子类必须嵌套在 Sealed Classes 类中,后来放宽了要求,禁止在 Sealed Classes 所定义的文件外使用, Kotlin 是如何做到的呢?如果我们在 Sealed Classes 所定义的文件外使用会怎么样?


image.png


正如你所看到,会导致编译错误,那么为什么 Sealed Classes 可以在同文件内使用呢?来看一下反编译后的代码。


// sealed
sealed class Color
// 同文件中使用 sealed class
class Red : Color()
// 以下是反编译代码 
... // 省略部分代码  
public final class Red extends Color {
   public Red() {
      super((DefaultConstructorMarker)null);
   }
}
... // 省略部分代码    
public abstract class Color {
   private Color() {
   }
   // $FF: synthetic method
   public Color(DefaultConstructorMarker $constructor_marker) {
      this();
   }
}
... // 省略部分代码


可以看到 Red class 被编译成了 final class, Sealed class 被编译成了 abstract class,同时编译器生成了一个 公有 的构造方法,其他的类无法直接调用,只有 Kotlin 编译器可以使用,Red class 被编译成 final class,在其构造方法内调用了 Color class 公有 的构造方法,而这些都是 Kotlin 编译器帮我们做的。


  • 构造函数私有化,限制了子类必须嵌套在 Sealed Classes 类中
  • 编译器生成了一个 公有 的构造方法,在子类的构造方法中调用了父类 公有 的构造方法,而这些都是 Kotlin 编译器帮我们做的


总结



枚举的局限性


  • 限制枚举每个类型只允许有一个实例
  • 限制所有枚举常量使用相同的类型的值


抽象类的局限性


对于一个抽象类我们可以用一些子类去继承它,但是子类不是固定的,它可以随意扩展,同时也失去枚举常量受限性。


枚举作为单例的优点


是否只有一个实例 是否反序列化 是否是线程安全 是否是懒加载


Sealed Classes 是什么?


Sealed 是一个 abstract 类,它本身是不能被实例化,只能用它的子类实例化对象。Sealed 的构造方法私有化,禁止在 Sealed 所定义的文件外使用。


  • Sealed Classes 用于表示受限制的类层次结构
  • 从某种意义上说,Sealed Classes 是枚举类的扩展
  • 枚举的不同之处在于,每个枚举常量仅作为单个实例存在,而 Sealed Classes 的子类可以表示不同状态的实例。


在什么情况下使用枚举或者 Sealed?


  • 如果涉及反序列化创建对象的时候,建议使用枚举,因为 Java 规定,枚举的序列化和反序列化是有特殊定制的,因此禁用编译器使用 writeObjectreadObjectreadObjectNoDatawriteReplacereadResolve 等方法。
  • 如果你不需要多次实例化,也不需要不提供特殊行为,或者也不需要添加额外的信息,仅作为单个实例存在,这个时候使用枚举更加合适。
  • 其他情况下使用 Sealed Classes,在一定程度上可以使用 Sealed Classes 代替枚举


自动补全 when 语句下的所有分支


推荐给大家一个快捷键 Mac/Win/Linux:Alt + Enter 可以补全 when 语句下的所有分支,效果如下所示:


image.png


参考文献




结语



公众号开通了:ByteCode , 欢迎小伙伴们前去查看 Android 10 系列源码,Jetpack ,Kotlin ,译文,LeetCode / 剑指 Offer / 国内外大厂算法题 等等一系列文章,如果对你有帮助,请帮我点个 star,感谢!!!,欢迎一起来学习,在技术的道路上一起前进。


正在建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,目前已经包含了 App Startup、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请仓库右上角帮我点个赞。


算法


由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。


  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……


每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。


Android 10 源码系列


正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。



Android 应用系列



精选译文


目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。



工具系列




目录
相关文章
|
Java Kotlin
Kotlin data数据类、copy()函数、sealed密封类
Kotlin data数据类、copy()函数、sealed密封类使用
115 0
|
IDE 编译器 开发工具
深入学习 Kotlin 特色之 Sealed Class 和 Interface
深入学习 Kotlin 特色之 Sealed Class 和 Interface
|
Kotlin
【Kotlin】Kotlin Sealed 密封类 ( 密封类声明 | 密封类子类定义 | 密封类特点 | 代码示例 )
【Kotlin】Kotlin Sealed 密封类 ( 密封类声明 | 密封类子类定义 | 密封类特点 | 代码示例 )
170 0
|
11天前
|
移动开发 Java Android开发
构建高效Android应用:Kotlin协程的实践之路
【4月更文挑战第30天】在移动开发领域,随着用户需求的不断增长和设备性能的持续提升,实现流畅且高效的用户体验已成为开发者的首要任务。针对Android平台,Kotlin协程作为一种新兴的异步编程解决方案,以其轻量级线程管理和简洁的代码逻辑受到广泛关注。本文将深入探讨Kotlin协程的概念、优势以及在实际Android应用中的运用,通过实例演示如何利用协程提升应用性能和响应能力,为开发者提供一条构建更高效Android应用的实践路径。
|
3天前
|
安全 Java Android开发
构建高效Android应用:采用Kotlin进行内存优化的策略
【5月更文挑战第8天】 在移动开发领域,性能优化一直是开发者关注的焦点。特别是对于Android应用而言,合理管理内存资源是确保应用流畅运行的关键因素之一。近年来,Kotlin作为官方推荐的开发语言,以其简洁、安全和互操作性的特点受到开发者青睐。本文将深入探讨利用Kotlin语言特性,通过具体策略对Android应用的内存使用进行优化,旨在帮助开发者提高应用性能,减少内存消耗,避免常见的内存泄漏问题。
7 0
|
4天前
|
Android开发 Kotlin
Kotlin开发Android之基础问题记录
Kotlin开发Android之基础问题记录
15 1
|
4天前
|
移动开发 数据库 Android开发
构建高效Android应用:Kotlin协程的全面应用
【5月更文挑战第7天】 在移动开发领域,性能优化与流畅的用户体验是至关重要的。随着Kotlin语言的流行,其并发神器——协程,已成为提升Android应用性能的重要工具。本文将深入探讨如何在Android项目中利用Kotlin协程进行异步编程、网络请求和数据库操作,以及如何通过协程简化代码结构,增强应用的响应性和稳定性。我们的目标是为开发者提供一套实用的协程使用模式和最佳实践,以便构建更加高效的Android应用。
20 3
|
4天前
|
移动开发 Java Android开发
Android应用开发:Kotlin语言的优势与实践
【5月更文挑战第7天】 在移动开发的世界中,Android平台的Kotlin语言以其高效、简洁的语法和强大的功能吸引了众多开发者。本文将深入探讨Kotlin语言的核心优势,并通过实际案例展示如何在Android应用开发中有效地运用这一现代编程语言。我们将从语言特性出发,逐步分析其在提升开发效率、改善代码质量以及增强应用性能方面的具体表现,为读者提供一个全面而细致的Kotlin应用开发指南。
|
4天前
|
移动开发 数据库 Android开发
构建高效Android应用:Kotlin与协程的完美结合
【5月更文挑战第7天】 在移动开发领域,性能优化和资源管理始终是核心议题。随着Kotlin语言的普及,其提供的协程特性为Android开发者带来了异步编程的新范式。本文将深入探讨如何通过Kotlin协程来优化Android应用的性能,实现流畅的用户体验,并减少资源消耗。我们将分析协程的核心概念,并通过实际案例演示其在Android开发中的应用场景和优势。
|
7天前
|
移动开发 前端开发 Android开发
构建高效Android应用:探究Kotlin协程的优势
【5月更文挑战第4天】 在移动开发领域,尤其是对于Android开发者而言,编写响应迅速且高效的应用程序至关重要。Kotlin作为一种现代的编程语言,其提供的协程特性为异步编程带来了革命性的改变。本文将深入探讨Kotlin协程在Android开发中的应用优势,并通过实例代码展示如何利用协程简化异步任务处理,提高应用性能和用户体验。