这周,我会讲到Java 8之后的一个非常重要的特性,就是密封类与接口。
这个特性并不是让代码更简洁的一个点,它是让Java的设计更健壮的一个特性。如果你希望在一些特别的场景下,设计出更健壮的程序。那密封类 Sealed Class就是你不可错过的一个特性。
Java是一门面向对象的语言,这个是我们众所周知的,而面向对象的语言的三大重要特性就是封装,继承与多态。
而在实际的场景中,我们经常会用上抽象与继承这个面向对象的特性。
子类可以继承父类,从而编写子类独特的属性与行为,任何依赖父类的业务,子类都可以替换掉它,这就是里氏替换原则。
在绝大多数情况下,这种继承的设计是非常有价值的。
除了少数情况以外。
举个实际场景的例子来说,在一些业务需求中,我们需要继承,但又期望限制继承的能力。
是不是听起来有点矛盾?
以星期为例,一周有七天,大多数场景下,我们都可能会有enum来实现这个。
代码可能是这样:
public enum Week {
Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
}
我相信绝大多数会这样来处理这个模型点。但枚举这种类型的使用能力有限,在简单的场景中这样定义并无问题。
但假设我们是在一个游戏的业务中,星期是游戏中一个很重要的概念,在业务上:
显而易见,就上述我描述的这个业务来说,使用枚举,并不是最佳实现方式。
也许我们使用抽象与继承可能会更合适。
定义一个Week的抽象父类
public abstract class Week {
/**
* 返回指定星期具有的独特的世界
**/
abstract World weekWorld();
/**
* 返回指定星期具有的特色任务
**/
abstract List<Task> specialWeekTask();
}
实现星期,以星期一为例
/**
* 这是星期一的世界
*/
public class Monday extends Week {
@Override
World weekWorld() {
return new MondayWorld();
}
@Override
List<Task> specialTasks() {
return List.of(new MondyTask());
}
}
如上代码所述,我们使用继承来实现这个业务,抽象了一个Week的父类,然后我们再各自实现不同的星期,并指定各种的世界与特别任务
在面向对象的语言中,这样的设计非常正常与常见。应该是Java程序员的家常便饭才是。
这就是继承。
继续这个游戏的世界,我们假设游戏中一个星期有七天,与现实一样。从星期一至星期日。不同的星期的世界如上述业务代码所述有所不同。
那从设计层面的问题就出来了:
如何限制一周只有七天
在枚举中,只要我们定义好,源码没有开放允许修改,那一周就是七天,谁也改变不了。
但如果是在继承的场景中,则完全不一样了。
假设考虑我们的游戏是不同的团队在合作,另一个团队创新式的定义了一个星期八
/**
* 第八天,这是一个特别的日期
*/
public class EightDay extends Week {
@Override
World weekWorld() {
return new TheEightWorld();
}
@Override
List<Task> specialTasks() {
return List.of(new EightDayTask());
}
}
从代码上来说,这是完全没有问题的。
但代码的没有问题不代表实际也允许这样。
考虑以下这些实际会发生的业务场景,我们就会发现Java的抽象与继承,在过往避免不了这种自己扩展定义子类以覆盖父类行为的编码行为。
比如在公司中,约束业务中,关键数据都要加密,并且公司级别提供了通用抽象加密接口及几种不同的加密实现,供实际团队挑选使用?结果有团队觉得加密没必要,自己实现了一个非加密的子类,绕过了公司层级的限制,在代码上并无问题,也难以被发现。
那怎么避免这样的场景?
如果你使用的是Java 8,除了用枚举或final class以外,只能依赖沟通与实际的非代码的约束来解决这种问题。
这就是密封类与接口要解决的问题。
密封类是这样一种概念,它在允许抽象与继承的基础之上,添加约束限制。
密封类或接口,允许你对于可实现的类或可继承的类进行约束,以防止类继承或实现被突破
还是以代码来展示更为直接。
密封类
//使用sealed关键字表明这是一个密封类
public abstract sealed class Week
//使用permits关键字来约束允许的子类或实现
permits Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday {
abstract World weekWorld();
abstract List<Task> specialTasks();
}
sealed class最先是在JDK 15做为预览特性引入,而后在JDK 17中,做为正式的特性被引入。
定义一个密封类或密封接口的原则是:
记住,类或接口,都可以使用sealed关键字。
要注意的是,继承或实现sealed class的子类,对于它本身,就是否进一步限制继承,同样有不同的策略。
final修饰
如果子类使用final来修饰,表明此子类申明自己不再允许被继承
selaed修饰
进一步约束子类又允许哪些子类来继承
no-sealed修饰
不限制继承,允许被随意继承
这三种策略,是基于类的继承而言,就sealed interface来说,实现只能是final。
这样,基于sealed的特性,你可以随心所欲定义出整个继承树的约束能力与限制。在一些特殊的业务场景中是非常有价值的。
另外,关于sealed class这个特性,在Kotlin,TypeScript中都有,理念与实现都非常相似,就不重复介绍了。
关于sealed class,概念上大家知道这么多就可以了。当然,关于更多语法上的细节,还是建议参照OpenJDK官网的说明来进一步了解。
事实上,每一个Java的版本,及其新特性,JDK官网都对这些点做了详细的说明与解释。我始终一再强调,关于具体的术的东西,官网永远是你最先访问的地方,不要舍近求远的找什么书,看什么博客,最先阅读的一定是官网提供的文档。
而我对于技术的文章,风格更多的是讲知其所以然,而不是知其然,我会更关注,为什么需要这个,它解决了过往什么问题,其它语言又是如何做的?
对于技术,知其所以然,比知其然更重要。
关于Java 8之后的新特性,这些是我认为从Java 9至Java 17中值得程序员关注的一些特性,因为这些特性如果你使用了新的Java,是可以很容易用上的。
还有一些类似什么JShell,Vector Api等一些特性,我个人感觉大多程序员很难用上,就没有聊这些了,但这不表示只有我讲的这么几个特性。
下一篇,本系列的终结篇:26岁的Java,为什么仍然是不可撼动的王者
我是御剑,一个致力于追求,实践与传播编码之道的程序员。
访问微言码道
(https://taoofcoding.tech)以阅读更多我写的文章;
访问myddd
(https://myddd.org)以了解我在维护的全栈式领域驱动开源框架。