写点什么

Java 语言新特性?封印类,mybatis 教程 pdf 下载

用户头像
极客good
关注
发布于: 刚刚
  • 封印类与记录类和模式匹配一起,为以数据为中心的编程模式提供支持。


Java SE 15(即将于 2020 年 9 月发布)引入封印类作为预览特性。封印类和接口对可扩展它们的子类型具有更多的控制权, 这对于一般的领域建模和构建更安全的平台库来说都是很有用的。


我们可以用 sealed 来声明一个类或接口,这意味着只有一组特定的类或接口可以直接对其进行扩展:


sealed interface Shape `` permits Circle, Rectangle { ... }


这段代码声明了一个叫作 Shape 的封印接口。permits 列表限制了只有“Circle”和“Shape”可以实现 Shape。(在某些情况下,编译器可以为我们推断出 permits 子句)。任何其他尝试扩展 Shape 的类或接口都将收到编译错误(如果你试图通过其他方式生成 Shape 子类,会在运行时出现错误)。


我们都知道可以通过 final 来限制扩展,而封印类可以被认为是广义的 final。限制可扩展的子类型将带来两个好处:超类型可以更好地指定可能的实现,而编译器可以更好地控制穷举(例如在 switch 语句或进行类型转换时)。封印类可与 记录类配对使用。


求和类型和乘积类型


上面的接口声明了 Shape 可以是 Circle 或 Rectangle,但不能是其他东西。换句话说,Shape 的集合等于 Circle 的集合加上 Rectangle 的集合。因此,封印类通常被称为求和(sum)类型,因为它们的值的集合是其他固定几种类型的值集合的总和。求和类型和封印类并不是什么新生事物,Scala 也有封印类,Haskell 和 ML 有用于定义求和类型的原语,有时候也被叫作标记联合(tagged union)或区分联合(discriminated union)。


求和类型经常与乘积类型一起使用。最近在 Java 中引入的记录类就是乘积类型,之所以被叫作乘积类型,是因为它们的状态空间是其组件的状态空间的笛卡尔乘积。(如果这么说听起来有点复杂,那么请将乘积类型看成元组,并将记录类看成名义上的元组)。让我们使用记录类继续声明 Shape 的子类型:


sealed interface Shape permits Circle, Rectangle { record Circle(Point center, int radius) implements Shape { } ``record Rectangle(Point lowerLeft, Point upperRight) implements Shape { } }


我们可以看到求和类型与乘积类型是如何结合在一起使用的。我们可以说“圆形是通过一个中心点和半径来定义的”、“矩形是通过两个点来定义的”以及“形状可以是圆形或矩形”。因为我们认为以这种方式共同声明基类及其实现是很常见的,所以当所有子类型都声明在同一编译单元中时,就可以省略 permits:


sealed interface Shape { record Circle(Point center, int radius) implements Shape { } record Rectangle(Point lowerLeft, Point upperRight) implements Shape { } }


这样是不是违反了封装性原则?


面向对象建模鼓励我们隐藏抽象类的实现,不建议我们问“Shape 可能的子类型是什么”之类的问题,并告诉我们向下转换到特定的实现类是一种“代码坏味道”。那么,为什么我们要引入这个似乎违反了这些原则的语言特性呢?(我


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


们也可以针对记录类提出同样的问题:要求在类表示与其 API 之间建立特定关系是不是违反了封装性原则?)


答案当然是“视情况而定”。在对抽象服务进行建模时,客户端通过抽象类型与服务进行交互可以降低耦合度,并最大限度地提高系统的演化灵活性。但是,在对特定领域进行建模时,如果该领域的特性已经是众所周知的,那么封装性原则可能就不一定会给我们带来多大好处。正如我们在记录类中所看到的那样,在对一些很普通的事物(例如点或 RGB 颜色)进行建模时,使用通用性对数据进行建模既需要做大量低价值的工作,而且更糟糕的是,这样通常会造成混淆。对于这种情况,封装性原则的成本已经超过了它的优势。


同样的结论也适用于封印类。在为一个简单且稳定的领域建模时,封装性原则并不一定会为我们带来好处,甚至还可能让客户端更加难以使用简单的领域内容。


当然,这并不说封装性原则是错误的,而是说成本和收益之间的权衡有时候不是那么明显。我们可以自己判断什么时候可以从中获得好处,什么时候会给我们造成阻碍。在选择是公开还是隐藏实现时,我们必须清楚封装性原则的好处和成本。通常,封装性是有好处的,但在为简单的领域建模时,封装性的好处可能会大打折扣。


如果一个类型,比如 Shape,限定了接口和实现类,我们就可以更放心地把它转成 Circle,因为 Shape 将 Circle 列为它的已知子类型之一。就像记录类是一种更透明的类,求和类型是一种更透明的多态性。这就是为什么求和类型和乘积类型会如此频繁一起出现。它们都代表了透明性和抽象性之间的某种折衷,因此,适合使用其中一个类型的地方也适合使用另一个类型。乘积和类型通常被称为 代数数据类型。


穷举


像 Shape 这样的封印类限定了一系列子类型,有助于程序员和编译器作出推断,而如果没有这些信息,我们就做不到。其他工具也可以利用这些信息。Javadoc 工具在生成的文档页面中列出了封印类允许的子类型。


Java SE 14 引入了一种有限定的 模式匹配,在未来会进一步扩展。第一个版本允许我们在 instanceof 中使用类型模式:


if (shape instanceof Circle c) { // 编译器已经为我们将 shape 转成 Circle 类型,并赋值给 c System.out.printf("Circle of radius %d%n", c.radius()); }


这离在 switch 中使用类型模式已经不远了。(Java SE 15 还不支持,但很快就会出现。) 到了那个时候,我们可以使用 switch 表达式(case 后面直接是类型)来计算一个形状的面积,如下所示:


float area = switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> Math.abs((r.upperRight().y() - r.lowerLeft().y()) * (r.upperRight().x() - r.lowerLeft().x())); // 不需要提供默认情况!``}


封印类在这里的作用是可以不使用默认子句,因为编译器从 Shape 的声明中已经知道 Circle 和 Rectangle 覆盖了所有形状,因此默认子句不会被执行。(编译器仍然会悄悄地在 switch 表达式中插入一个默认子句,这样做是为了防止在编译和运行这段时间内子类型发生变化,但没有必要让程序员来做这件事情。) 这类似于对枚举进行 switch,因为枚举覆盖了所有已知的常量,所以也不需要使用默认子句。(对于这种情况,忽略默认子句通常会更好,因为使用默认子句好像在提醒我们是不是错过了某种情况)。


Shape 的继承结构给了客户端一个选择:它们可以完全通过抽象接口使用形状,也可以“展开”抽象,并在必要时与更具体的形状发生交互。模式匹配等特性使这种“展开”更易于阅读和编写。


代数数据类型示例


“乘积和”模式非常强大。最好的情况是,子类型列表不发生变化,并预计客户端会直接区分子类型,这样会更容易,也更有用。


限定一组固定的子类型,并鼓励客户端直接使用这些子类型,这是一种紧耦合的形式。在所有条件相同的情况下,我们鼓励使用松耦合的设计,以最大限度地提高灵活性,但这种松耦合也是要付出代价的。在编程语言中同时使用“不透明”和“透明”的抽象可以让我们根据实际情况选择合适的工具。


我们可能已经在 java.util.concurrent.Future API 中使用了一系列乘积和 (如果当时这是一种选择的话)。Future 表示可以与其发起者并发执行的计算,Future 所代表的计算可能还没有开始、已经开始但还没有完成、已经成功完成(或已经完成但出现异常)、已经超时或被中断取消。Future 的 get() 方法反映了所有这些可能性:


interface Future<V> { ... V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;``}


如果计算尚未完成,get() 会一直阻塞,直到完成。如果是成功的,则返回计算结果。如果抛出异常,异常将被封装在 ExecutionException 中。如果计算超时或被中断,则会抛出另一种异常。这个 API 非常精确,但使用起来有些痛苦,因为它有多个控制路径,不管是普通路径 (get() 返回一个值) 还是失败路径,都必须在 catch 块中处理:


try { V v = future.get(); // 处理一般的完成情况}catch (TimeoutException e) { // 处理超时}catch (InterruptedException e) { // 处理取消}catch (ExecutionException e) { Throwable cause = e.getCause(); // 处理失败``}


如果在 Java 5 引入 Future 时,我们已经有封印类、记录类和模式匹配,那么我们可能会这样定义返回类型:


sealed interface AsyncReturn<V> { record Success<V>(V result) implements AsyncReturn<V> { } record Failure<V>(Throwable cause) implements AsyncReturn<V> { } record Timeout<V>() implements AsyncReturn<V> { } record Interrupted<V>() implements AsyncReturn<V> { }}...interface Future<V> {` `AsyncReturn<V> get();}


在这里,异步结果可以是成功 (包含返回值)、失败 (包含异常)、超时或取消。这是对可能出现的结果更为统一的描述,而不是用返回值描述其中的一些结果,再用异常描述另一些结果。客户端仍然需要处理所有的情况——无法回避任务可能会失败的事实——但我们可以统一地 (并更紧凑地) 处理这些情况(见脚注):


AsyncResult<V> r = future.get();switch (r) {` `case Success(var result): ...` `case Failure(Throwable cause): ...` `case Timeout(), Interrupted(): ...}


乘积和是一种广义的枚举

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
Java语言新特性?封印类,mybatis教程pdf下载