基本上,当没有具体状态可以在不同成员之间共享时,给出一个密封的层次结构。这就是实现接口和扩展类之间的主要区别 - 接口没有自己的字段或构造函数。
但在某种程度上,这不是一个重要的问题。真正的问题是,为什么你想要一个密封的层次结构开始。一旦确定这一点,密封接口的位置应该会更清晰。
(提前为例子的人为性和冗长冗长的行为道歉)
1. 使用子类化而不使用“为子类化而设计”。
假设你有一个这样的类,它位于你已经发布的库中。
public final class Airport {
private List<String> peopleBooked;
public Airport() {
this.peopleBooked = new ArrayList<>();
}
public void bookPerson(String name) {
this.peopleBooked.add(name);
}
public void bookPeople(String... names) {
for (String name : names) {
this.bookPerson(name);
}
}
public int peopleBooked() {
return this.peopleBooked.size();
}
}
现在,您希望向图书馆添加一个新版本,该版本将打印出预订时预订人员的姓名。有几种可能的路径可以执行此操作。
如果你是从头开始设计的,你可以合理地用一个接口替换类,并设计成用类似的东西来组合。Airport
Airport
PrintingAirport
BasicAirport
public interface Airport {
void bookPerson(String name);
void bookPeople(String... names);
int peopleBooked();
}
public final class BasicAirport implements Airport {
private final List<String> peopleBooked;
public Airport() {
this.peopleBooked = new ArrayList<>();
}
@Override
public void bookPerson(String name) {
this.peopleBooked.add(name);
}
@Override
public void bookPeople(String... names) {
for (String name : names) {
this.bookPerson(name);
}
}
@Override
public int peopleBooked() {
return this.peopleBooked.size();
}
}
public final class PrintingAirport implements Airport {
private final Airport delegateTo;
public PrintingAirport(Airport delegateTo) {
this.delegateTo = delegateTo;
}
@Override
public void bookPerson(String name) {
System.out.println(name);
this.delegateTo.bookPerson(name);
}
@Override
public void bookPeople(String... names) {
for (String name : names) {
System.out.println(name);
}
this.delegateTo.bookPeople(names);
}
@Override
public int peopleBooked() {
return this.peopleBooked.size();
}
}
这在我们的假设中是不可行的,因为该类已经存在。将会有对和方法的调用,这些方法期望某些特定类型的东西,除非我们使用继承,否则无法以向后兼容的方式保留。Airport
new Airport()
Airport
因此,要完成 java 15 之前的操作,您需要从类中删除 并编写子类。final
public class Airport {
private List<String> peopleBooked;
public Airport() {
this.peopleBooked = new ArrayList<>();
}
public void bookPerson(String name) {
this.peopleBooked.add(name);
}
public void bookPeople(String... names) {
for (String name : names) {
this.bookPerson(name);
}
}
public int peopleBooked() {
return this.peopleBooked.size();
}
}
public final class PrintingAirport extends Airport {
@Override
public void bookPerson(String name) {
System.out.println(name);
super.bookPerson(name);
}
}
在这一点上,我们遇到了继承最基本的问题之一 - 有很多方法可以“打破封装”。因为 中的方法碰巧在内部调用,所以我们的类按设计工作,因为它的新方法最终将为每个人调用一次。bookPeople
Airport
this.bookPerson
PrintingAirport
bookPerson
但是,如果将类更改为此,Airport
public class Airport {
private List<String> peopleBooked;
public Airport() {
this.peopleBooked = new ArrayList<>();
}
public void bookPerson(String name) {
this.peopleBooked.add(name);
}
public void bookPeople(String... names) {
for (String name : names) {
this.peopleBooked.add(name);
}
}
public int peopleBooked() {
return this.peopleBooked.size();
}
}
则子类将无法正常运行,除非它也被覆盖。进行反向更改,除非它没有覆盖,否则它不会正常运行。PrintingAirport
bookPeople
bookPeople
这不是世界末日或任何东西,它只是需要考虑和记录的事情 - “你如何扩展这个类以及你被允许覆盖什么”,但是当你有一个开放扩展的公共类时,任何人都可以扩展它。
如果您跳过了如何子类化的文档,或者没有足够多的文档,那么很容易最终导致这样一种情况,即您无法控制的使用您的库或模块的代码可能依赖于您现在陷入困境的超类的一小部分细节。
通过密封类,您可以通过仅针对所需的类将超类打开到扩展来暂缓步骤。
public sealed class Airport permits PrintingAirport {
// ...
}
现在,您不需要向外部消费者记录任何内容,只需记录自己即可。
那么接口如何适应这种情况呢?好吧,假设您确实提前考虑了一下,并且您拥有通过组合添加功能的系统。
public interface Airport {
// ...
}
public final class BasicAirport implements Airport {
// ...
}
public final class PrintingAirport implements Airport {
// ...
}
您可能不确定以后是否不想使用继承来保存类之间的一些重复,但是由于您的 Airport 接口是公开的,因此您需要创建一些中间接口或类似的东西。abstract class
你可以防御并说“你知道吗,直到我对我希望这个API去哪里有了更好的了解,我将成为唯一能够实现接口的人”。
public sealed interface Airport permits BasicAirport, PrintingAirport {
// ...
}
public final class BasicAirport implements Airport {
// ...
}
public final class PrintingAirport implements Airport {
// ...
}
2. 表示具有不同形状的数据“案例”。
假设您向 Web 服务发送请求,它将在 JSON 中返回两个内容之一。
{
"color": "red",
"scaryness": 10,
"boldness": 5
}
{
"color": "blue",
"favorite_god": "Poseidon"
}
当然,这有点人为的,但你可以很容易地想象一个“类型”字段或类似的字段来区分将会出现哪些其他字段。
因为这是Java,所以我们要将原始的无类型JSON表示形式映射到类中。让我们来看看这种情况。
一种方法是有一个类包含所有可能的字段,并且只具有一些依赖字段。null
public enum SillyColor {
RED, BLUE
}
public final class SillyResponse {
private final SillyColor color;
private final Integer scaryness;
private final Integer boldness;
private final String favoriteGod;
private SillyResponse(
SillyColor color,
Integer scaryness,
Integer boldness,
String favoriteGod
) {
this.color = color;
this.scaryness = scaryness;
this.boldness = boldness;
this.favoriteGod = favoriteGod;
}
public static SillyResponse red(int scaryness, int boldness) {
return new SillyResponse(SillyColor.RED, scaryness, boldness, null);
}
public static SillyResponse blue(String favoriteGod) {
return new SillyResponse(SillyColor.BLUE, null, null, favoriteGod);
}
// accessors, toString, equals, hashCode
}
虽然这在技术上是有效的,因为它确实包含所有数据,但在类型级安全性方面并没有那么大的收获。任何获得a的代码都需要知道在访问对象的任何其他属性之前检查自身,并且需要知道哪些属性是安全的。SillyResponse
color
我们至少可以制作一个枚举而不是一个字符串,这样代码就不需要处理任何其他颜色,但它仍然远非理想。随着不同案件变得更加复杂或数量越多,情况就越糟。color
理想情况下,我们想要做的是为您可以打开的所有情况提供一些通用的超类型。
由于不再需要打开该属性,因此该属性不是绝对必要的,但根据个人品味,您可以将其保留为可在界面上访问的内容。color
public interface SillyResponse {
SillyColor color();
}
现在,这两个子类将具有不同的方法集,并且可以使用任何一个子类的代码来确定它们具有的方法集。instanceof
public final class Red implements SillyResponse {
private final int scaryness;
private final int boldness;
@Override
public SillyColor color() {
return SillyColor.RED;
}
// constructor, accessors, toString, equals, hashCode
}
public final class Blue implements SillyResponse {
private final String favoriteGod;
@Override
public SillyColor color() {
return SillyColor.BLUE;
}
// constructor, accessors, toString, equals, hashCode
}
问题是,由于是一个公共接口,任何人都可以实现它,并且不一定是唯一可以存在的子类。SillyResponse
Red
Blue
if (resp instanceof Red) {
// ... access things only on red ...
}
else if (resp instanceof Blue) {
// ... access things only on blue ...
}
else {
throw new RuntimeException("oh no");
}
这意味着这种“哦不”的情况总是会发生。
题外话:在java 15之前,为了解决这个问题,人们使用了“类型安全访问者”模式。为了你的理智,我建议不要学习,但如果你很好奇,你可以看看ANTLR生成的代码 - 它都是一个由不同“形状”的数据结构组成的大层次结构。
密封类让你说“嘿,这些是唯一重要的情况。
public sealed interface SillyResponse permits Red, Blue {
SillyColor color();
}
即使这些案例共享零个方法,该接口的功能也可以与“标记类型”一样好,并且仍然为您提供一个类型,以便在您期望其中一种情况时写入。
public sealed interface SillyResponse permits Red, Blue {
}
此时,您可能会开始看到与枚举的相似之处。
public enum Color { Red, Blue }
枚举说“这两个实例是唯一的两种可能性”。他们可以有一些方法和字段。
public enum Color {
Red("red"),
Blue("blue");
private final String name;
private Color(String name) {
this.name = name;
}
public String name() {
return this.name;
}
}
但是所有实例都需要具有相同的方法和相同的字段,并且这些值必须是常量。在密封的层次结构中,您可以获得相同的“这是唯一的两种情况”保证,但是不同的情况可以具有非恒定的数据和彼此之间的不同数据 - 如果这有意义的话。
“密封接口+ 2个或更多记录类”的整个模式相当接近像rust的枚举这样的结构所期望的。
这同样适用于具有不同行为“形状”的一般对象,但它们没有自己的要点。
3. 强制不变量
有一些不变量,如不可变性,如果允许子类,则无法保证。
// All apples should be immutable!
public interface Apple {
String color();
}
public class GrannySmith implements Apple {
public String color; // granny, no!
public String color() {
return this.color;
}
}
这些不变量可能会在代码的后面被依赖,比如在将对象提供给另一个线程或类似线程时。使层次结构密封意味着您可以记录并保证比允许任意子类化更强的不变量。
封顶
密封接口或多或少与密封类具有相同的用途,只有当您希望在类之间共享实现时,您才使用具体的继承,这超出了默认方法之类的东西所能提供的范围。