深入理解 Java17 新特性:Sealed Classes

0 关键总结

  • Java SE 15在2020年9月发布,预览功能引入“封闭类”(JEP 360)
  • 封闭类是一种限制哪些其他类或接口可扩展它的类或接口
  • 类似枚举,封闭类在领域模型中捕获替代方案,允许程序员和编译器推理其穷尽性
  • 封闭类对于创建安全的层次结构也很有用,通过解耦可访问性和可扩展性,允许库开发者公开接口,同时控制所有实现
  • 封闭类与记录和模式匹配一起工作,以支持更数据中心化的编程形式

1 预览功能

鉴于Java全球影响力和高兼容性承诺,语言功能设计错误代价非常高。如语言功能存在缺陷,保持兼容性不仅意味很难移除或显著改变功能,且现有功能还会限制未来功能发展。新功能要通过实际使用来验证,开发人员的反馈至关重要。为确保在快速发布节奏下有足够的时间进行实验和反馈,新语言功能将通过一或多个轮次的预览来测试,这些功能是平台的一部分,但需要单独选择进入,并且尚未成为永久功能,以便在根据开发人员的反馈进行调整时,不会破坏关键代码。

Java SE 15(2020年9月)引入了作预览功能。封闭允许类和接口更好地控制其允许的子类型,这对于一般领域建模和构建更安全的平台库都很有用。

一个类或接口可以声明为sealed,这意味着只有特定的一组类或接口可以直接扩展它:

sealed interface Shapepermits Circle, Rectangle { ... }

这声明了一个名为Shape的封闭接口。permits列表表示只有CircleRectangle可以实现Shape。(在某些情况下,编译器可以为我们推断出允许列表。)任何其他尝试扩展Shape的类或接口将会收到编译错误(或在运行时尝试生成声明Shape为超类型的非标签类文件时,收到运行时错误)。

我们已熟悉通过final类限制扩展;封闭可被认为是终结性的泛化。限制允许的子类型集可能带来两个好处:

  • 超类型的作者可以更好地推理可能的实现,因为他们可以控制所有的实现
  • 而编译器可以更好地推理穷尽性(例如在switch语句或强制转换中)

封闭类与[记录]配合得很好。

2 和枚举类型类似的和积类型

上面的接口声明表明,一个Shape可以是CircleRectangle,而不能是其他任何东西。即所有Shape的集合等于所有Circle的集合加上所有Rectangle的集合。因此,封闭类通常被称为和类型,因为它们的值集是其他类型固定列表的值集的总和。封闭类和和类型不是新事物,如Scala和Haskell都有封闭类,而ML有定义和类型的原语(有时称为标签联合判别联合)。

和类型经常与积类型一起出现。记录是最近[引入Java]的积类型形式,因为它们的状态空间是其组件的状态空间的笛卡尔积的一个子集(如果这听起来复杂,可以将积类型想象为元组,记录是命名元组)。

用记录完成Shape的声明:

sealed interface Shapepermits 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 { } 
}

3 违反封装?

历史上,对象建模鼓励隐藏抽象类型的实现集。被告知不要问“可能的Shape子类型是什么”,类似地被告知向特定实现类的下转型是种“代码异味”。

为啥现在添加看似违反这些长期原则的语言功能?(也可问类似问题,关于记录:要求在类的表示和其API之间建立特定关系是否违反封装?)

当然是“视情况而定”。建模一个抽象服务时,通过抽象类型与服务交互是一个积极的好处,因为减耦,并最大限度提高系统演进灵活性。但建模一个特定领域时,如该领域特性已很清楚,封装可能没太多优势。正如记录中所见,建模如XY点或RGB颜色这样简单数据时,使用对象的完全通用性来建模数据需要大量低价值工作,更糟糕的,往往掩盖实际发生的事。此时,封装成本不值得其带来的好处;将数据建模为数据更简单直接。

同样的论点适用于封闭类。建模一个已知且稳定的领域时,“我不会告诉你有哪些种类的形状”的封装可能不会带来我们期望从不透明抽象中获得的好处,甚至可能使客户更难处理一个实际上很简单的领域。

这不意味着封装是个错误;这仅意味着有时成本和收益的平衡不一致,可通过判断来确定何时有帮助,何时妨碍。当选择公开或隐藏实现时,须明确封装的收益和成本。它是否为我们提供演进实现的灵活性或仅是个信息破坏的障碍,阻碍对方已显而易见的东西?封装的好处通常巨大,但在建模已知领域的简单层次结构时,声明坚如磐石的抽象的开销有时可能超过收益。

Shape这样的类型不仅承诺其接口,还承诺实现它的类时,可更好询问“你是圆形吗”并转换为Circle,因为Shape明确命名Circle作为其已知子类型之一。就像记录是一种更透明的类,和类型是一种更透明的多态性。这就是为啥和类型和积类型如此频繁一起出现;它们都代表透明性和抽象之间的权衡,所以在一个地方有意义的地方,另一个地方也可能有意义。(和积类型通常被称为代数数据类型。)

4 穷尽性

Shape这样的封闭类承诺一个可能子类型的穷尽列表,这有助于程序员和编译器以我们以前无法做到的方式推理形状。(其他工具也可以利用这些信息;Javadoc工具在生成的封闭类文档页面中列出了允许的子类型。)

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

if (shape instanceof Circle c) {// 编译器已为我们将shape转换为Circle,并绑定到cSystem.out.printf("Circle of radius %d%n", c.radius()); 
}

从那里易跳到在switch中使用类型模式。可用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()));// no default needed!
}

封闭的贡献在无需default子句,因为编译器从Shape的声明中知道CircleRectangle覆盖了所有的形状,因此switch中的default子句将不可达。(编译器仍会在switch表达式中默默地插入一个抛出默认子句,以防Shape的允许子类型在编译和运行时之间发生变化,但没有必要坚持程序员编写这个“以防万一”的默认子句。)这类似我们对待另一个穷尽性的来源——覆盖所有已知常量的enum上的switch表达式也不需要default子句(在这种情况下省略它通常是个好主意,因为这更有可能提醒我们错过了一个情况。)

Shape这样的层次结构为其客户端提供一个选择:他们可完全通过抽象接口处理形状,但他们也可在有意义时“展开”抽象并通过更明确的类型进行交互。像模式匹配这样的语言特性使这种展开更易读写。

5 代数数据类型示例

“和积模式”可以是一种强大的模式。为了适用,它必须极不可能更改子类型列表,并且我们预见到让客户端直接区分子类型会更容易和更有用。

承诺一个固定的子类型集,并鼓励客户端直接使用这些子类型,是一种紧耦合。一般,我们被鼓励在设计中使用松耦合,以最大限度提高更改灵活性,但这种松耦合也有成本。语言中同时拥有“不透明”和“透明”抽象允许我们为特定情况选择合适工具。

一个可能会使用和积类型的地方是在java.util.concurrent.FutureAPI。Future代表一个可能与其发起者并发运行的计算;Future表示的计算可能尚未开始,已开始但尚未完成,已成功完成或异常完成,已超时或被取消。Futureget()反映所有这些可能性:

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(): ...
}

6 和积类型是广义的枚举

理解和积类型的一个好方法是,它们是枚举的广义形式。一个枚举声明声明了一个具有穷尽常量实例集的类型:

enum Planet { MERCURY, VENUS, EARTH, ... }

可将数据与每个常量关联,如行星的质量和半径:

enum Planet {MERCURY (3.303e+23, 2.4397e6),VENUS (4.869e+24, 6.0518e6),EARTH (5.976e+24, 6.37814e6),...
}

广义而言,一个封闭类枚举的不是封闭类的固定实例列表,而是固定实例类型的种类列表。如这个封闭接口列出各种类型的天体及与每种类型相关的数据:

sealed interface Celestial {record Planet(String name, double mass, double radius)implements Celestial {}record Star(String name, double mass, double temperature)implements Celestial {}record Comet(String name, double period, LocalDateTime lastSeen)implements Celestial {}
}

正如你可穷尽地切换枚举常量,你也可以穷尽地切换各种天体类型:

switch (celestial) {case Planet(String name, double mass, double radius): ...case Star(String name, double mass, double temp): ...case Comet(String name, double period, LocalDateTime lastSeen): ...
}

这种模式的例子随处可见:UI系统中的事件,面向服务系统中的返回码,协议中的消息等。

7 更安全的层次结构

到目前为止,我们讨论了封闭类在将替代方案纳入领域模型时的有用性。封闭类还有另一个完全不同的应用:安全层次结构。

Java一直允许我们通过将类标记为final来表示“这个类不能被扩展”。final存在承认了一个关于类的基本事实:有时它们被设计为可扩展,有时则不是,希望支持这两种模式。实际上,[Effective Java]建议我们“设计和记录用于扩展,否则禁止它”。这是很好的建议,如语言能更多帮助我们,可能更常被采纳。

可惜,语言在两方面未能帮助我们:

  • 类的默认设置是可扩展,而非 final
  • 并且final机制实际相当弱,因为它迫使作者在限制扩展和使用多态作为实现技术之间做出选择

String是个很好例子,平台安全性要求字符串不可变,因此String不能公开扩展——但对实现来说有多个子类型会非常方便。(解决这个问题的成本很高;[紧凑字符串]通过对仅包含Latin-1字符的字符串进行特殊处理,提供显著的内存占用和性能改进,但若String是封闭类而非final类,这会更容易和低成本。)

通过使用包私有构造函数并将所有实现放在同一包,模拟封闭类(但不是接口)效果的技巧众所周知。这有帮助,但仍不舒服,公开一个不打算扩展的公共抽象类。库作者更喜欢使用接口来公开不透明的抽象;抽象类被设计为一种实现辅助工具,而不是建模工具。(参见[Effective Java],“优先使用接口而不是抽象类”。)

使用封闭接口,库作者无需在使用多态作为实现技术、允许不受控制的扩展或将抽象公开为接口之间做出选择——他们可三者兼得。作者可能选择让实现类可访问,但更可能的是,实现类将保持封装。

封闭类允许库作者解耦可访问性和可扩展性。拥有这种灵活性很好,但啥时应该使用它呢?当然,我们不会想要封闭像List这样的接口——用户创建新的List类型是完全合理且可取的。封闭可能有:

  • 成本(用户无法创建新实现)
  • 和收益(实现可以全局推理所有实现)

我们应该将封闭保留给收益超过成本时。

8 细则

sealed修饰符可用于类或接口。尝试封闭一个final类,无论:

  • 显式声明的final修饰符
  • 还是隐式final,如枚举和记录类

都是错误的。

封闭类有个permits列表,是唯一允许的直接子类型,它们必须:

  • 在封闭类编译时可用
  • 实际是封闭类的子类型
  • 封闭类在同一模块(或在未命名模块中则在同一个包中)

这要求实际上意味着它们必须与封闭类共同维护,这是对这种紧耦合的合理要求。

若允许的子类型都在封闭类的同一编译单元中声明,可省略permits子句,并推断为同一编译单元中声明的所有子类型。封闭类不能用作lambda表达式的函数接口,也不能用作匿名类的基类型。

封闭类的子类型必须更明确地说明其可扩展性;封闭类的子类型须sealedfinal或显式标记为non-sealed。(记录和枚举隐式为final,因此不需要显式标记。)如果类或接口没有封闭的直接超类型,标记为non-sealed是错误的。

将现有final类变为sealed是二进制和源代码兼容的。对于你不控制所有实现的非final类,将其封闭既不二进制兼容也不源代码兼容。将新的允许子类型添加到封闭类是二进制兼容但不源代码兼容的(这可能会破坏switch表达式的穷尽性)。

9 总结

封闭类有多种用途;它们在领域建模技术中很有用,当捕获领域模型中的穷尽替代方案时;在解耦可访问性和可扩展性时,它们也是有用的实现技术。封闭类型是[记录]的自然补充,因为它们共同形成了一种称为代数数据类型的常见模式;它们也是[模式匹配]的自然契合。 关注我,紧跟本系列专栏文章,咱们下篇再续!

作者简介:魔都架构师,多家大厂后端一线研发经验,在分布式系统设计、数据平台架构和AI应用开发等领域都有丰富实践经验。

各大技术社区头部专家博主。具有丰富的引领团队经验,深厚业务架构和解决方案的积累。

负责:

  • 中央/分销预订系统性能优化

  • 活动&券等营销中台建设

  • 交易平台及数据中台等架构和开发设计

  • 车联网核心平台-物联网连接平台、大数据平台架构设计及优化

  • LLM Agent应用开发

  • 区块链应用开发

    目前主攻市级软件项目设计、构建服务全社会的应用系统。

参考:

  • 编程严选网

    本文由博客一文多发平台 OpenWrite 发布!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/49633.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

iOS 创建一个私有的 CocoaPods 库

创建一个私有的 CocoaPods 库&#xff08;pod&#xff09;涉及几个步骤&#xff0c;包括设置私有的 Git 仓库、创建 Podspec 文件、发布到私有仓库等等。以下是详细步骤&#xff1a; 设置私有 Git 仓库 首先&#xff0c;在 GitHub、GitLab 或 Bitbucket 上创建一个新的私有仓库…

身份证如何查验真伪?C#身份证二要素、三要素接口集成

身份证不仅是我们的身份证明&#xff0c;更是社会生活中的“通行证”&#xff0c;现在人们的衣食住行都离不开身份证。但对于提供服务的平台而言&#xff0c;如何对用户提供的身份信息进行真伪核验便成为了一大难题。别担心&#xff0c;今天小编为服务平台带来了身份证二要素、…

MySQL:增删改查、临时表、授权相关示例

目录 概念 数据完整性 主键 数据类型 精确数字 近似数字 字符串 二进制字符串 日期和时间 MySQL常用语句示例 SQL结构化查询语言 显示所有数据库 显示所有表 查看指定表的结构 查询指定表的所有列 创建一个数据库 创建表和列 插入数据记录 查询数据记录 修…

Android 列表视频滑动自动播放—滑动过程自动播放(滑动播放页面卡顿优化)

上一篇文章我们RecyclerView列表滑动自动播放——Android 列表视频滑动自动播放—滑动过程自动播放(实现思路) 实际我们会发现滑动页面自动播放视频页面会卡顿&#xff0c;针对这个问题&#xff0c;我们可以优化一下代码&#xff0c;通过Handler.runnable()去实现计算播放视频…

力扣 232用栈实现队列

思路&#xff1a; 栈的特性是先进后出&#xff0c;队列是先进先出 因此用两个栈来模拟队列 要实现的功能包括 push 入队列 pop() 出队列 peek获取队列的最上元素 isempty 队列判空 push 正常操作 stin.push()&#xff0c;只要元素入栈就行&#xff0c;stout元素…

C++ Map Set的模拟实现

C Map Set的模拟实现 文章目录 前言一、Map 和 Set是什么&#xff1f;1.Set2.Map 二、困难点困难一、set和map中值的类型不同困难二、Map和Set中值不可修改困难三、红黑树中迭代器的和--1.2.- - 困难四、map中[ ] 运算符重载的实现1.修改红黑树以及Map和Set中insert的返回值1.修…

Three.js投射光线实现三维物体交互

<template><div id"webgl"></div> </template><script setup> import * as THREE from three //导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls // 导入 dat.gui import { GUI } from thre…

k8s v1.30 完整安装过程及CNI安装过程总结

博主未授权任何人或组织机构转载博主任何原创文章&#xff0c;感谢各位对原创的支持&#xff01; 博主链接 本人就职于国际知名终端厂商&#xff0c;负责modem芯片研发。 在5G早期负责终端数据业务层、核心网相关的开发工作&#xff0c;目前牵头6G技术研究。 博客内容主要围绕…

【ffmpeg命令入门】添加水印

文章目录 前言什么是水印&#xff1f;为什么要添加水印&#xff1f;ffmpeg添加水印添加图片水印添加文字水印基本使用方法drawtext的参数 总结 前言 在视频制作和编辑的过程中&#xff0c;添加水印是一个常见且重要的步骤。水印不仅可以保护版权&#xff0c;还能用于品牌宣传和…

使用LLaMA-Factory对Llama3-8B-Chinese-Chat进行微调

文章目录 模型及数据&#xff1a;模型下载数据 LLaMA-Factory启动拉取代码启动webui 模型训练数据导入数据预览设置模型路径配置参数及参数的保存开始训练 过程观察加载模型、对话模型导出、再次加载 模型及数据&#xff1a; 模型下载 使用基于中文数据训练过的 LLaMA3 8B 模…

同步状态的广播事件

定向活动广播 你可以直接将事件从一个状态广播到另一个状态&#xff0c;以同步同一图表中的并行&#xff08;AND&#xff09;状态。以下规则适用&#xff1a; 在事件广播期间&#xff0c;接收状态必须处于活动状态。 一个图表中的操作无法将事件广播到另一个图表的状态。 与无定…

大坝安全监测设备有哪些主要功能?

推荐型号&#xff1a;TH-WY1】大坝安全监测设备的主要功能包括以下几个方面&#xff1a; 1. **实时监测大坝的各项物理参数**&#xff1a;包括应变、位移、水位、流量等<sup>1</sup><sup>2</sup>。 2. **数据处理和分析**&#xff1a;对监测数据进行处…

CSS全部标签属性重置语法

简单介绍 每个浏览器默认的属性不同&#xff0c;像谷歌浏览器不设置的话&#xff0c;默认每个元素是有外边距margin的&#xff0c;有的浏览器列表还会带上序号&#xff0c;有的浏览器就没有。 所以为了让所有浏览器都一致&#xff0c;在写CSS开始前&#xff0c;我们都需要用C…

[Javascript】前端面试基础3【每日学习并更新10】

Web开发中会话跟踪的方法有那些 cookiesessionurl重写隐藏inputip地址 JS基本数据类型 String&#xff1a;用于表示文本数据。Number&#xff1a;用于表示数值&#xff0c;包括整数和浮点数。BigInt&#xff1a;用于表示任意精度的整数。Boolean&#xff1a;用于表示逻辑值…

【React1】React概述、基本使用、脚手架、JSX、组件

文章目录 1. React基础1.1 React 概述1.1.1 什么是React1.1.2 React 的特点声明式基于组件学习一次,随处使用1.2 React 的基本使用1.2.1 React的安装1.2.2 React的使用1.2.3 React常用方法说明React.createElement()ReactDOM.render()1.3 React 脚手架的使用1.3.1 React 脚手架…

c生万物系列(封装)

为了对c语言进行封装&#xff0c;笔者参考了lw_oopc等开源库&#xff0c;决定使用宏对结构体进行封装。 先说一下大致思想&#xff1a;通过宏&#xff0c;结构体和文件来实现封装。 大概步骤&#xff1a;抽象出类-> 使用lw_oopc库进行封装->定义接口封装底层实现 ->…

在Ubuntu 14.04上安装和使用Memcache的方法

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 简介 随着您的网站的增长和流量的增加&#xff0c;最快显示压力的组件之一是后端数据库。如果您的数据库没有分布式和配置来处理高负载…

【常见开源库的二次开发】基于openssl的加密与解密——SHA算法源码解析(六)

目录 一、SHA-1算法分析&#xff1a; 1.1 Merkle Tree可信树 1.2 源码实现&#xff1a; 1.3 哈希计算功能 1.4 两种算法的区别&#xff1a; 1.4.1 目的 1.4.2 实现机制 1.4.3 输出 1.4.4 应用场景&#xff1a; 1.4 运行演示&#xff1a; 二、SHA-2算法分析&#xff1a; 2.1哈…

JavaScript 实现对象长度的获取

JavaScript 实现对象长度的获取 在 JavaScript 中&#xff0c;要获取对象的长度&#xff08;即对象中属性的数量&#xff09;&#xff0c;可以使用以下几种方法&#xff1a; 使用 Object.keys()&#xff1a;该方法返回一个对象自身&#xff08;即不包括继承属性&#xff09;可…

责任链模式的应用与解析

目录 责任链模式责任链模式结构责任链模式适用场景责任链模式优缺点练手题目题目描述输入描述输出描述题解 责任链模式 责任链模式&#xff0c;亦称职责链模式、命令链&#xff0c;是一种行为设计模式&#xff0c;允许你将请求沿着处理者链进行发送。收到请求后&#xff0c;每…