密封类sealed class

一、什么是kotlin密封类?

密封类是一种特殊的类,它用来表示受限的类继承结构,即一个类只能有有限的几种子类,而不能有任何其他类型的子类。

密封类使用sealed关键字声明,在Kotlin 1.0中,密封类的所有子类必须嵌套在密封类内部;在Kotlin 1.1中,这个限制放宽了,允许将子类定义在同一个文件中;在Kotlin 1.5中,这个限制进一步放宽了,允许将子类定义在任何地方,只要保证子类可见性不高于父类。

密封类最大的优点是可以配合when表达式实现完备性检查(exhaustive check),即编译器可以检测出是否覆盖了所有可能的分支情况,如果没有则会报错或提示警告。这样可以避免遗漏某些情况导致逻辑错误或运行时异常。

例如,我们可以定义一个表示运算符的密封类,以及它的四个子类:

//定义一个密封类
sealed class Operator

//定义四个子类,分别表示加、减、乘、除
class Add : Operator()
class Subtract : Operator()
class Multiply : Operator()
class Divide : Operator()

二、kotlin密封类的优缺点

2.1 优点

  • 密封类可以让我们在编译时就确定所有可能的子类类型,从而提高代码的安全性和可读性。
  • 密封类可以让我们在使用when表达式时,不需要写else分支,因为所有的情况都已经覆盖了,这样可以减少代码的冗余和错误。
  • 密封类可以让我们在设计模式中,更方便地实现一些模式,例如状态模式、访问者模式等。

2.2 缺点

  • 密封类的子类必须在同一个文件中声明,或者嵌套在密封类的声明中,这样可能会导致文件过长或者结构不清晰。
  • 密封类的子类不能被其他类继承,这样可能会限制了代码的复用性和扩展性。

三、kotlin密封类的使用方式

密封类适合用于表示有限的几种类型的情况,例如:

  • 表示状态或者结果,例如成功、失败、加载中等。
  • 表示运算符或者表达式,例如加、减、乘、除等。
  • 表示事件或者动作,例如点击、滑动、拖拽等。

下面我们用一个简单的示例来演示如何使用密封类。我们定义一个表示计算器的密封类,它有两个子类,分别表示数字和运算符:

//定义一个表示计算器的密封类
sealed class Calculator

//定义两个子类,分别表示数字和运算符
data class Number(val value: Int) : Calculator()
data class Operator(val op: String) : Calculator()

然后我们定义一个函数,用来根据输入的密封类对象,计算结果:


//定义一个函数,用来根据输入的密封类对象,计算结果
fun calculate(a: Calculator, b: Calculator): Int {
    //使用when表达式,根据不同的子类类型,进行不同的操作
    return when (a) {
        //如果a是数字,再判断b的类型
        is Number -> when (b) {
            //如果b也是数字,就根据a的值和b的值进行运算
            is Number -> when (a.value) {
                //如果a的值是0,就返回0
                0 -> 0
                //如果a的值是1,就返回b的值
                1 -> b.value
                //否则,就根据b的运算符进行运算
                else -> when (b.op) {
                    //如果b的运算符是加号,就返回a的值加b的值
                    "+" -> a.value + b.value
                    //如果b的运算符是减号,就返回a的值减b的值
                    "-" -> a.value - b.value
                    //如果b的运算符是乘号,就返回a的值乘b的值
                    "*" -> a.value * b.value
                    //如果b的运算符是除号,就返回a的值除b的值
                    "/" -> a.value / b.value
                    //其他情况,就抛出异常
                    else -> throw IllegalArgumentException("Invalid operator: ${b.op}")
                }
            }
            //如果b是运算符,就抛出异常
            is Operator -> throw IllegalArgumentException("Cannot calculate two operators")
        }
        //如果a是运算符,就抛出异常
        is Operator -> throw IllegalArgumentException("Cannot calculate an operator and a number")
    }
}

四、密封类与设计模式

1.状态模式

状态模式是一种行为型设计模式,它允许一个对象在其内部状态改变时改变它的行为。状态模式将状态封装成独立的类,并将请求委托给当前的状态对象,从而实现状态的切换和行为的变化。

1.1kotlin实现

密封类可以表示一个有限的状态集合,而且可以在when表达式中进行完备的检查,不需要使用else分支。例如,假设有一个电灯类,它有两种状态:开和关,每种状态下可以执行不同的操作。可以用以下的代码来实现:

// 定义一个密封类表示状态
sealed class State {
    // 定义一个抽象方法表示操作
    abstract fun operate()
}

// 定义一个开状态的子类,继承自State
class On : State() {
    // 重写操作方法,打印信息并切换到关状态
    override fun operate() {
        println("The light is on, turn it off.")
        Light.state = Off()
    }
}

// 定义一个关状态的子类,继承自State
class Off : State() {
    // 重写操作方法,打印信息并切换到开状态
    override fun operate() {
        println("The light is off, turn it on.")
        Light.state = On()
    }
}

// 定义一个电灯类,它有一个状态属性,初始为关状态
object Light {
    var state: State = Off()

    // 定义一个方法,根据当前状态执行操作
    fun switch() {
        state.operate()
    }
}

// 测试代码
fun main() {
    // 创建一个电灯对象
    val light = Light
    // 调用switch方法,根据当前状态执行操作
    light.switch() // The light is off, turn it on.
    light.switch() // The light is on, turn it off.
    light.switch() // The light is off, turn it on.
}
1.2java实现

在java中,实现状态模式的一种方式是使用枚举类,因为枚举类也可以表示一个有限的状态集合,而且可以实现接口或抽象类,从而定义不同的行为。例如,用java实现上面的例子,可以用以下的代码:

// 定义一个接口表示状态
interface State {
    // 定义一个抽象方法表示操作
    void operate();
}

// 定义一个枚举类表示状态,实现State接口
enum LightState implements State {
    // 定义两个枚举常量,分别表示开和关状态
    // 在每个枚举常量的构造器中,传入一个State对象,表示该状态下的行为
    ON(new State() {
        // 重写操作方法,打印信息并切换到关状态
        @Override
        public void operate() {
            System.out.println("The light is on, turn it off.");
            Light.state = OFF;
        }
    }),
    OFF(new State() {
        // 重写操作方法,打印信息并切换到开状态
        @Override
        public void operate() {
            System.out.println("The light is off, turn it on.");
            Light.state = ON;
        }
    });

    // 定义一个私有的State属性,表示该枚举常量对应的状态对象
    private final State state;

    // 定义一个私有的构造器,接收一个State对象作为参数,赋值给state属性
    private LightState(State state) {
        this.state = state;
    }

    // 定义一个公共的方法,调用state属性的operate方法
    public void operate() {
        state.operate();
    }
}

// 定义一个电灯类,它有一个状态属性,初始为关状态
class Light {
    // 定义一个静态的LightState属性,表示电灯的状态,初始为OFF
    public static LightState state = LightState.OFF;

    // 定义一个方法,根据当前状态执行操作
    public void switch() {
        state.operate();
    }
}

// 测试代码
public class Main {
    public static void main(String[] args) {
        // 创建一个电灯对象
        Light light = new Light();
        // 调用switch方法,根据当前状态执行操作
        light.switch(); // The light is off, turn it on.
        light.switch(); // The light is on, turn it off.
        light.switch(); // The light is off, turn it on.
    }
}

可以看出,使用kotlin的密封类实现状态模式的优点是:

  • 代码更简洁,不需要定义接口或抽象类,也不需要使用匿名内部类
  • 代码更安全,不需要担心在其他地方出现未知的状态,也不需要使用else分支处理默认情况
  • 代码更易读,可以清楚地看出状态之间的层次关系和转换逻辑

使用java的枚举类实现状态模式的优点是:

  • 代码更统一,不需要在不同的类中定义状态,也不需要使用对象来表示状态
  • 代码更高效,不需要创建多个状态对象,也不需要使用多态来调用操作方法

2. 访问者模式

访问者模式是一种行为型设计模式,它允许在不修改已有类的结构的情况下,定义作用于这些类的新操作。访问者模式将元素的结构和元素的操作分离,使得操作可以根据不同的元素类型而变化。

2.1kotlin实现

在kotlin中,可以使用密封类来实现访问者模式,因为密封类可以表示一个有限的元素集合,而且可以在when表达式中进行完备的检查,不需要使用else分支。例如,假设有一个表达式类,它有两种子类:数字和加法,每种子类都可以被访问者访问,执行不同的操作。可以用以下的代码来实现:

// 定义一个密封类表示表达式
sealed class Expr {
    // 定义一个抽象方法表示接受访问者的访问
    abstract fun <R> accept(visitor: Visitor<R>): R
}
    // 定义一个数字的子类,继承Expr类,有一个value属性表示数字的值
    data class Num(val value: Int) : Expr() {
        // 重写accept方法,调用访问者的visitNum方法,传入自己作为参数
        override fun <R> accept(visitor: Visitor<R>): R {
            return visitor.visitNum(this)
        }
    }

    // 定义一个加法的子类,继承Expr类,有两个Expr属性表示左右操作数
    data class Sum(val left: Expr, val right: Expr) : Expr() {
        // 重写accept方法,调用访问者的visitSum方法,传入自己作为参数
        override fun <R> accept(visitor: Visitor<R>): R {
            return visitor.visitSum(this)
        }
    }

    // 定义一个访问者接口,泛型参数R表示访问的结果类型
    interface Visitor<R> {
        // 定义一个访问数字的方法,接收一个Num对象作为参数,返回一个R类型的结果
        fun visitNum(num: Num): R
        // 定义一个访问加法的方法,接收一个Sum对象作为参数,返回一个R类型的结果
        fun visitSum(sum: Sum): R
    }

    // 定义一个求值的访问者类,实现Visitor接口,泛型参数为Int
    class EvalVisitor : Visitor<Int> {
        // 重写访问数字的方法,返回数字的值
        override fun visitNum(num: Num): Int {
            return num.value
        }
        // 重写访问加法的方法,返回左右操作数的求值结果的和
        override fun visitSum(sum: Sum): Int {
            return sum.left.accept(this) + sum.right.accept(this)
        }
    }

    // 定义一个打印的访问者类,实现Visitor接口,泛型参数为String
    class PrintVisitor : Visitor<String> {
        // 重写访问数字的方法,返回数字的字符串表示
        override fun visitNum(num: Num): String {
            return num.value.toString()
        }
        // 重写访问加法的方法,返回加法的字符串表示,用括号括起来
        override fun visitSum(sum: Sum): String {
            return "(${sum.left.accept(this)} + ${sum.right.accept(this)})"
        }
    }

    // 测试代码
    fun main() {
        // 创建一个表达式对象,表示1 + (2 + 3)
        val expr = Sum(Num(1), Sum(Num(2), Num(3)))
        // 创建一个求值的访问者对象
        val evalVisitor = EvalVisitor()
        // 创建一个打印的访问者对象
        val printVisitor = PrintVisitor()
        // 调用表达式的accept方法,传入求值的访问者,打印求值的结果
        println(expr.accept(evalVisitor)) // 6
        // 调用表达式的accept方法,传入打印的访问者,打印表达式的字符串表示
        println(expr.accept(printVisitor)) // (1 + (2 + 3))
    }

可以看出,使用kotlin的密封类实现访问者模式的优点是:

  • 代码更简洁,不需要定义抽象方法或接口,也不需要使用类型转换或类型检查
  • 代码更安全,不需要担心在其他地方出现未知的表达式类型,也不需要使用else分支处理默认情况
  • 代码更易读,可以清楚地看出表达式之间的层次关系和访问者的操作逻辑
2.2 java实现
// 抽象元素
interface Animal {
    void accept(Visitor visitor); // 接受一个抽象访问者
}

// 具体元素Lion
class Lion implements Animal {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this); // 调用具体访问者对自己进行操作
    }

    public String roar() {
        return "Roar!";
    }
}

// 具体元素Tiger
class Tiger implements Animal {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this); // 调用具体访问者对自己进行操作
    }

    public String growl() {
        return "Growl!";
    }
}

// 具体元素Elephant
class Elephant implements Animal {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this); // 调用具体访问者对自己进行操作
    }

    public String trumpet() {
        return "Trumpet!";
    }
}

// 抽象访问者
interface Visitor {
    void visit(Lion lion); // 访问具体元素Lion

    void visit(Tiger tiger); // 访问具体元素Tiger

    void visit(Elephant elephant); // 访问具体元素Elephant
}

// 具体访问者Feed
class Feed implements Visitor {
  @Override 
  public void visit(Lion lion) { 
      System.out.println("Feed the lion with meat.");
  } 

  @Override 
  public void visit(Tiger tiger) { 
      System.out.println("Feed the tiger with chicken.");
  } 

  @Override 
  public void visit(Elephant elephant) { 
      System.out.println("Feed the elephant with banana.");
  } 
}

// 具体访问者Observe
class Observe implements Visitor { 
  @Override 
  public void visit(Lion lion) { 
      System.out.println("Observe the lion: " + lion.roar());
  } 

  @Override 
  public void visit(Tiger tiger) { 
      System.out.println("Observe the tiger: " + tiger.growl());
  } 

  @Override 
  public void visit(Elephant elephant) { 
      System.out.println("Observe the elephant: " + elephant.trumpet());
  }  
}

// 具体访问者Train
class Train implements Visitor {  
   @Override  
   public void visit(Lion lion) {  
       System.out.println("Train the lion to jump through a hoop.");  
   }  

   @Override  
   public void visit(Tiger tiger) {  
       System.out.println("Train the tiger to stand on a ball.");  
   }  

   @Override  
   public void visit(Elephant elephant) {  
       System.out.println("Train the elephant to sit on a stool.");  
   }   
}  

// 对象结构类Zoo

class Zoo {

 private List<Animal> animals = new ArrayList<>();

 // 添加一个新动物   
public void add(Animal animal) {   
     animals.add(animal);   
}   

 // 移除一个已有动物   
public void remove(Animal animal) {   
     animals.remove(animal);   
}   

 // 接受一个抽象访问者,并将所有动物传递给它进行处理    
public void accept(Visitor visitor) {    
     for (Animal animal : animals) {    
         animal.accept(visitor);    
     }    
}   
}

测试代码:

public class Test {

   public static void main(String[] args) {

       Zoo zoo = new Zoo();

       zoo.add(new Lion());
       zoo.add(new Tiger());
       zoo.add(new Elephant());

       Visitor feed = new Feed();
       Visitor observe = new Observe();
       Visitor train = new Train();

       zoo.accept(feed);
       zoo.accept(observe);
       zoo.accept(train);
   }
}

运行测试代码并显示输出结果:

Feed the lion with meat.
Feed the tiger with chicken.
Feed the elephant with banana.
Observe the lion: Roar!
Observe the tiger: Growl!
Observe the elephant: Trumpet!
Train the lion to jump through a hoop.
Train the tiger to stand on a ball.
Train the elephant to sit on a stool.

五、与final类对比

特性 密封类 final类
可继承性 部分可继承,只能被指定的类继承 不可继承
受保护成员或虚成员 不允许,因为密封类的子类必须是密封类或final类 允许,因为final类没有子类
抽象性 允许,因为密封类可以是抽象的或具体的 不允许,因为抽象类必须被继承
相似之处 都是限制类的继承,都不能声明为抽象的,都可以继承别的类或接口 都是限制类的继承,都不能声明为抽象的,都可以继承别的类或接口
不同之处 可以指定哪些类可以作为其子类,可以实现多态性,可以用于一些特定的设计模式 不能有任何子类,不能实现多态性,不能用于一些特定的设计模式
优点 可以保证封装性和多态性,可以提高代码的可读性和可维护性,可以避免不必要的继承或实现 可以保证封装性和不变性,可以提高代码的执行效率,可以避免类的滥用
缺点 可能增加代码的复杂度和冗余,可能限制类的扩展性和灵活性,可能与一些框架或库不兼容 可能增加代码的耦合度和僵化度,可能限制类的扩展性和灵活性,可能与一些框架或库不兼容
应用场景 可以用于实现一些特定的设计模式,例如状态模式、策略模式、访问者模式等,这些模式需要明确地定义一组有限的子类或实现类 可以用于实现一些不需要继承的类,例如工具类、常量类、单例类等,这些类需要保证其不变性和唯一性
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容