基本上是在没有具体状态可以在不同成员之间共享时提供密封的层次结构。这是实现接口和扩展类之间的主要区别 - 接口没有自己的字段或构造函数。
但在某种程度上,这不是重要的问题。真正的问题是为什么您想要一个密封的层次结构。一旦确定,密封接口的安装位置应该会更清楚。
(提前为示例的做作和冗长表示歉意)
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
.
这不是世界末日或其他什么,它只是需要考虑和记录的事情 - “如何扩展这个类以及你允许重写什么”,但是当你有一个可供扩展的公共类时anyone可以延长它。
如果您跳过记录如何子类化或记录得不够多,那么很容易陷入这样的情况:您无法控制的使用库或模块的代码可能依赖于您现在所坚持的超类的一个小细节。
密封类可以让您通过打开您的超类来仅扩展您想要的类来避免这种情况。
public sealed class Airport permits PrintingAirport {
// ...
}
现在,您不需要向外部消费者记录任何内容,只需向您自己记录即可。
那么接口如何适应这一点呢?好吧,假设您确实提前考虑了并且您拥有通过组合添加功能的系统。
public interface Airport {
// ...
}
public final class BasicAirport implements Airport {
// ...
}
public final class PrintingAirport implements Airport {
// ...
}
您可能不确定您是否don't稍后想使用继承来节省类之间的一些重复,但由于您的 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
and 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 之前,为了解决这个问题,人们使用了“类型安全访客”模式 https://en.wikipedia.org/wiki/Visitor_pattern。为了您的理智,我建议不要学习它,但如果您好奇,您可以查看代码ANTLR https://en.wikipedia.org/wiki/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;
}
}
但所有实例都需要有same方法和same字段和这些值需要是常量。在密封的层次结构中,您可以获得相同的“这是唯一的两种情况”保证,但不同的情况可以具有非常量数据和彼此不同的数据 - 如果这是有意义的。
“密封接口 + 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;
}
}
稍后在代码中可能会依赖这些不变量,例如将对象提供给另一个线程或类似线程时。使层次结构密封意味着您可以记录并保证比允许任意子类化更强大的不变量。
封顶
密封接口或多或少与密封类具有相同的用途,只有当您想要在类之间共享超出默认方法之类的实现的实现时,才使用具体继承。