颠覆认知:一向主张可扩展性的Java,为何要推出封闭类?

简介: 本文介绍了Java的Sealed Classes(封闭类)功能,探讨了为何Java在强调可扩展性的同时引入这一特性。文章基于JDK 17.0.5,详细解释了Sealed Classes的概念及其作用。通过对比final类和package-private类,阐述了封闭类在提高安全性和控制扩展性方面的优势。最后,通过具体示例展示了如何使用sealed关键字以及相关语法。了解这一新特性有助于我们更好地把握Java未来的发展趋势。

你好,我是猿java。

当你还在 JDK 8驰骋沙场,大张旗鼓搞可扩展性时,JDK 15却已暗度陈仓:"偷偷摸摸"搞起了 Sealed Classes(封闭类)的功能,为何一向主张可扩展性的 Java,却会反其道而行之,推出封闭类这个功能?今天就让我们一起来聊聊这期中的原委。

申明:本文基于 jdk-17.0.5

2020年,给 JDK 15增加 Sealed Classes(封闭类)的提案被提交,在经历了 JDK 16版本的迭代后,最终该功能在 JDK 17正式发布,Sealed Classes发展的 JEP如下:

jdk-sealed.png

JEP: JDK Enhancement Proposal, JDK增强建议,JEP是一个JDK核心技术相关的增强建议文档;

什么是封闭类

Sealed Classes:翻译为 密封类、封闭类。代表该类/接口是一个封闭的类/接口,只有许可的类/接口 才能继承或实现该类/接口。如下图来自 JDK真实封闭类源码:

sealed-interface.png

jdk-sealed-class.png

因此,用 sealed关键字修饰的类就是封闭类。

为什么需要封闭类

像Java 这种面向对象的编程语言,可扩展性是衡量设计优劣的一个重要指标,可是 Java为何要引入封闭类? 这不是明晃晃的限制扩展性吗?

其实,这是安全性和扩展性权衡的一个结果。

可能有小伙伴会说:控制继承能力,用 Java现有的方式就ok,为何还需要多增加一个封闭类呢?

那我们先看看 Java现有的 2种控制继承能力的方法:

  • 用关键字 final修饰类,类就成了终态类,无法被继承;比如我们经常使用的 String类。

final-string.png

将类定义为 final后,类就完全失去了扩展性,简单粗暴,适合完全不需要扩展的类/接口。

  • package-private类,也就是非 public类,这样只有同包下的类才能继承,比如:java.nio中的 Bits类;

package-private.png

将类申明为非 public(package-private),只有同包下的类才能继承,尽管在同包下还是保留了扩展,但是可能会出现下面的安全问题。

再来看看继承带来的安全问题:

如下代码为 java.io.FileCleanable 源码,该类主要是用来清理文件。在 JDK中,FileCleanable类被申明成 final,也就意味着该类不能被继承,不能被扩展了,为什么要把FileCleanable类申明成 final呢?

final class FileCleanable extends PhantomCleanable<FileDescriptor> {
   

    // 省略部分代码
    @Override
    public void clear() {
   
      if (remove()) {
   
        super.clear();
      }
    }
}

假如去掉 FileCleanable类的final关键字,因此就可以实现自定义类 MyFileCleanable去继承 MyFileCleanable类,并且覆写 clear(),或者新增add()方法,代码示例如下,这样会带来什么问题呢?

public class MyFileCleanable extends FileCleanable {
   

    // 省略代码
    @Override
    public void clear() {
   
      // 实现自定义文件删除逻辑,删除所有文件
      cearAllFolders();
    }

    public void add(){
   

    }
}

原本 FileCleanable类中的 clear方法只能JVM 内部调用,如果开放扩展性,自定义类就可以覆写clear()方法,因此可以任意修改 clear()的逻辑,假如用户A 本来并没有操作clear()的权限,但是因为类的继承扩展而获得了该权限,如果不小心删除了重要文件,结果可能是直接去财务室结账走人...

因此子类继承父类,获得扩展性主要会带来2个影响:

  • 子类可以覆写父类中的现有方法;
  • 子类可以为父类添加新方法;

上述两个影响都可能带来安全性的问题,因此我们在设类时需要多思考一些安全性相关的问题:

  • 类有没有必要被子类扩展,被扩展后会不会有安全问题,该类能不能被定义成 final?
  • 类中的方法,被子类拿到会不会有安全问题,能不能被定义成 final?

综上,使用 final和 package-private 两种限制方式的粒度都比较粗,不限制的话又可能带来一些安全隐患,所以我们就会想,有没有粒度细一点又能解决安全隐患的问题,因此,封闭类就"粉墨登场"了。

如何使用封闭类

有了封闭类,要如何使用呢?我们先看下 JDK 12引入的几个源码类(在jdk 17 中被加上了 sealed修饰):

public sealed interface ConstantDesc
  permits ClassDesc,
  MethodHandleDesc,
  MethodTypeDesc,
  Double,
  DynamicConstantDesc,
  Float,
  Integer,
  Long,
  String {
   

  // 省略代码
}

public sealed interface ClassDesc
        extends ConstantDesc,
                TypeDescriptor.OfField<ClassDesc>
        permits PrimitiveClassDescImpl,
                ReferenceClassDescImpl {
   

    // 省略代码
}

final class PrimitiveClassDescImpl
  extends DynamicConstantDesc<Class<?>> implements ClassDesc {
   

  // 省略代码
}

public abstract non-sealed class DynamicConstantDesc<T>
  implements ConstantDesc {
   

  // 省略代码
}

在解释上述源码之前,铺垫 JDK 17中因密封类而增加了几个重要关键词:

  1. sealed:(封闭)用于修饰类/接口,代表这个类/接口为密封类/接口;
  2. non-sealed:(非封闭)用于修饰类/接口,代表这个类/接口为非密封类/接口;
  3. permits:(允许)用于 extends和 implements之后,指定能够继承或实现封闭类的子类/接口;

了解了 JDK17增加的几个关键字之后,我们再来分析上面的源码,从源码中我们可以发现,sealed 通常和 permits关键字一起使用。

首先通过关键字 sealed申明一个封闭的接口 ConstantDesc,然后用 permits关键字允许 ClassDesc,
MethodHandleDesc,
MethodTypeDesc,
Double,
DynamicConstantDesc,
Float,
Integer,
Long,
String 能够实现或者继承 ConstantDesc这个接口。

然后,扩展类 PrimitiveClassDescImpl 定义成 final,代表它是一个终态类,无法被继承,DynamicConstantDesc 被定义成 non-sealed,代表它的子类还可以继续继承扩展它。

最后,我们对封闭类总结如下:

  1. 申明封闭类:在类前面加 sealed关键字;
  2. 给封闭类开放扩展权限:在类声明后加上 permits关键字,后面加上需要授权的类;
  3. 许可类可以申明成 final,关闭扩展性;可以声明为 sealed,延续扩展性;也可以申明成 non-sealed,支持扩展性不受限;
  4. permits 关键字指定的许可子类(permitted subclasses),必须和封闭类处于同一模块(module)或者包空间(package)里;

总结

  • sealed通常和 permits关键字一起使用;
  • sealed封闭类提供了更精细粒度的可扩展性;
  • sealed封闭类可以更好地控制代码的安全性和健壮性;
  • 尽管目前国内大部分业务的代码还是运行在 JDK8环境上,但是了解 JDK的新特性对可以帮助我们更好的了解它以后的一个发展趋势;

学习交流

文章总结不易,如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注:猿java,持续输出硬核文章。

目录
相关文章
|
18天前
|
Java Linux
java基础(3)安装好JDK后使用javac.exe编译java文件、java.exe运行编译好的类
本文介绍了如何在安装JDK后使用`javac.exe`编译Java文件,以及使用`java.exe`运行编译好的类文件。涵盖了JDK的安装、环境变量配置、编写Java程序、使用命令行编译和运行程序的步骤,并提供了解决中文乱码的方法。
38 1
|
18天前
|
Java 索引
java基础(13)String类
本文介绍了Java中String类的多种操作方法,包括字符串拼接、获取长度、去除空格、替换、截取、分割、比较和查找字符等。
29 0
java基础(13)String类
|
6天前
|
Java
Java Object 类详解
在 Java 中,`Object` 类是所有类的根类,每个 Java 类都直接或间接继承自 `Object`。作为所有类的超类,`Object` 定义了若干基本方法,如 `equals`、`hashCode`、`toString` 等,这些方法在所有对象中均可使用。通过重写这些方法,可以实现基于内容的比较、生成有意义的字符串表示以及确保哈希码的一致性。此外,`Object` 还提供了 `clone`、`getClass`、`notify`、`notifyAll` 和 `wait` 等方法,支持对象克隆、反射机制及线程同步。理解和重写这些方法有助于提升 Java 代码的可读性和可维护性。
|
11天前
|
Java API
Java的日期类都是怎么用的
【10月更文挑战第1天】本文介绍了 Java 中处理日期和时间的三个主要类:`java.util.Date`、`java.util.Calendar` 和 `java.time` 包下的新 API。`Date` 类用于表示精确到毫秒的瞬间,可通过时间戳创建或获取当前日期;`Calendar` 抽象类提供丰富的日期操作方法,如获取年月日及时区转换;`java.time` 包中的 `LocalDate`、`LocalTime`、`LocalDateTime` 和 `ZonedDateTime` 等类则提供了更为现代和灵活的日期时间处理方式,支持时区和复杂的时间计算。
29 14
|
28天前
|
存储 安全 Java
Java——String类详解
String 是 Java 中的一个类,用于表示字符串,属于引用数据类型。字符串可以通过多种方式定义,如直接赋值、创建对象、传入 char 或 byte 类型数组。直接赋值会将字符串存储在串池中,复用相同的字符串以节省内存。String 类提供了丰富的方法,如比较(equals() 和 compareTo())、查找(charAt() 和 indexOf())、转换(valueOf() 和 format())、拆分(split())和截取(substring())。此外,还介绍了 StringBuilder 和 StringJoiner 类,前者用于高效拼接字符串,后者用于按指定格式拼接字符串
27 1
Java——String类详解
|
19天前
|
存储 Java
Java编程中的对象和类
【8月更文挑战第55天】在Java的世界中,“对象”与“类”是构建一切的基础。就像乐高积木一样,类定义了形状和结构,而对象则是根据这些设计拼装出来的具体作品。本篇文章将通过一个简单的例子,展示如何从零开始创建一个类,并利用它来制作我们的第一个Java对象。准备好让你的编程之旅起飞了吗?让我们一起来探索这个神奇的过程!
27 10
|
16天前
|
安全 Java 编译器
java访问类字段
java访问类字段
|
18天前
|
Java
java的class类
java的class类
21 5
|
24天前
|
Java 开发者
Java Character 类详解
Java中的`Character`类是`java.lang`包的一部分,用于将基本类型`char`封装为对象,并提供了丰富的静态方法来处理字符,如类型判断、大小写转换等。
|
18天前
|
Java 数据处理
Java Scanner 类详解
`Scanner` 类是 Java 中 `java.util` 包提供的强大工具,用于从多种输入源(如键盘、文件、字符串)读取数据。本文详细介绍如何创建 `Scanner` 对象并使用其常用方法(如 `next()`, `nextInt()`, `nextLine()` 等)。通过示例代码展示如何从标准输入、字符串及文件中读取数据,并进行输入验证。使用时需注意关闭 `Scanner` 以释放资源,并确保输入类型匹配,避免异常。掌握 `Scanner` 可显著提升程序的数据处理能力。