User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Kotlin   ›   Kotlin Notes   ›   正文
Kotlin Notes

Kotlin-类型进阶—密封类(二十六)

2021-01-27 23:56:17
1261  0 1
参考目录 隐藏
1) 密封类
2) 密封类定义
3) 密封类和 when 的使用
4) 密封与枚举对比
5) 枚举和抽象类的局限性
6) 限制所有枚举常量使用相同的类型的值
7) 抽象类的局限性
8) Sealed Classes
9) Sealed Classes 用于表示受限制的类层次结构
10) 在什么情况下使用枚举或者 Sealed?
11) 密封类的实例:递归整型列表的简单实现
12) Kotlin 中的密封类 优于 带标签的类
13) Tagged Classes 是什么
14) 类的层次结构

阅读完需:约 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()
        }
    }
}

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

1 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593740浏览

随机文章
Java—加密扩展(JCE)框架 之 Cipher-加密与解密
4年前
Caffeine—缓存实战
2年前
SpringCloud—OpenFeign拦截器应用(RequestInterceptor)
5年前
Ajax简介和发送异步请求(四步操作)
5年前
ActiveMQ—入门(消息中间件)
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593740 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付