密封类的作用
在面向对象语言中,我们可以通过继承(extend)来实现类的能力复用、扩展与增强。但有的时候,有些能力我们不希望被继承了去做一些不可预知的扩展。所以,我们需要对继承关系有一些限制的控制手段。而密封类的作用就是限制类的继承。
已有的限制手段
对于继承能力的控制,Java很早就已经有一些了,主要是这两种方式。
1,final修饰类,这样类就无法被继承了。
2,package-private类(非public类),可以控制智能被同一个包下的类继承。
上面两种方式控制的粒度都是非常粗,如果有更精细化的限制需求的话,是很难实现的。
新特性
为了进一步增强限制能力,java17中的密封类增加了几个关键词:
- sealed:修饰类/接口,为了描述这个类/接口为密封类/接口
- non-sealed: 修饰类/接口,为了描述这个类/接口为非密封类/接口
- petmits:用在extends或者implement之后,指定可以继承或实现的类。
下面我们通过一个例子来理解这几个关键词的用法。
假设我们设计王者荣耀这个游戏,这个游戏给用户选择的英雄分为五大类:
- 坦克
- 射手
- 法师
- 辅助
- 刺客
每个种类下面又有各种不同的英雄。所以从我们传统的面向设计思路,会这样来创建。
// 英雄基类
public class Hero {
}
// 坦克英雄的抽象
public class TankHero extends Hero {
}
// 射手英雄的抽象
public class AdcHero extends Hero {
}
// 法师英雄的抽象
public class MageHero extends Hero {
}
// 刺客英雄的抽象
public class AssassinHero extends Hero {
}
// 辅助英雄的抽象
public class SupportHero extends Hero {
}
// 坦克英雄:亚瑟
public class YaSe extends TankHero {
}
// 输出英雄:后羿
public class HouYi extends AttackHero {
}
// 输出英雄:妲己
public class DaJi extends MageHero {
}
// 刺客英雄:猴子
public class HouZi extends AssassinHero {
}
// 辅助英雄:张飞
public class ZhangFei extends SupportHero {
}
整体结构有三层,具体如下图所示:
第一层:Hero是所有英雄的基类,定义英雄的基础属性
第二层:按英雄的分类的五个不同抽象,定义英雄的公共属性。
第三层:具体的英雄定义。
这个时候为了避免开发人员在创建英雄的时候,搞乱这样的三层结构,就可以通过引入密封类的特性来做限制。
这个场景我们对于第一层和第二层是稳定的,对于第二层英雄的种类的抽象不允许在增加,此时我们就可以这样写:
public sealed class Hero permits TankHero, AdcHero, MageHero,AssassinHero,SupportHero {
}
通过sealed关键词和permitspermists关键来定义Hero是一个需要密封的类,并且它的子类只允许为TankHero, AdcHero, MageHero,AssassinHero,SupportHero这五个。
改造完成之后,我们会发现TankHero, AdcHero, MageHero,AssassinHero,SupportHero这五个类开始报错了,具体错误如下。
sealed, non-sealed or final modifiers expected
这是因为父类Hero被sealed修饰之后,sealed的密封要求被传递过来,此时子类就必须在sealed、non-sealed、final之间选择一个定义,她们分别代表:
- sealed:继承延续密封类特性,可以继续指定继承的类,并传递密封定义给子类。
- non-seale:声明这个类为非密封类,可以被任意继承
- final:不允许继承。
在这个场景下,第一层和第二层稳定,允许第三层具体英雄角色可以后期不断增加新英雄,所以三类抽象英雄可以这样编写。
public non-sealed class TankHero extends Hero {
}
对于第三层英雄角色,已经是具体实现,则可以使用final定义来阻断后续的继承关系。
public final class YaSe extends AttackHero {
}
通过这样的设置,这三层英雄的结构第一和第二层就得到了比较好的保护。