系列中的上一篇
当前教程
强封装(JDK 内部)

强封装(JDK 内部)

几乎所有依赖项 - 无论是框架、库、JDK API 还是您自己的(子)项目 - 都具有公开的、受支持的、稳定的 API 以及使公开部分正常工作所需的内部代码。强封装是关于避免(意外)使用内部 API 以使项目更加健壮和可维护。我们将探讨为什么需要这样做,究竟什么是内部 API(特别是对于 JDK),以及强封装在实践中是如何工作的。

注意:您需要了解模块系统基础知识以及关于反射支持才能充分利用本文。

什么是强封装?

在许多方面,OpenJDK 代码库与任何其他软件项目类似,一个不变的是重构。代码被更改、移动、删除等,以保持代码库的整洁和可维护性。当然,并非所有代码都是这样:公共 API,与 Java 用户的契约,非常稳定。

如您所见,公共 API 和内部代码之间的区别对于维护兼容性至关重要,这对 JDK 开发人员和您来说都是如此。您需要确保您的项目,即您的代码您的依赖项,不依赖于任何次要 JDK 更新中可能发生更改的内部内容,从而导致意外且不必要的工作。更糟糕的是,这种依赖关系可能会阻止您更新 JDK。同时,您可能处于内部 API 提供独特功能的情况,而没有这些功能,您的项目将无法竞争。

总之,这意味着默认情况下锁定内部 API 但允许您为特定用例解锁特定 API 的机制至关重要。强封装就是这种机制。

由于只有导出或打开的包中的类型可以在模块外部访问,因此所有其他内容都被视为内部,因此无法访问。首先,这适用于 JDK 本身,自 Java 9 以来,JDK 被拆分为模块。

什么是内部 API?

那么哪些 JDK API 是内部的?要回答这个问题,我们需要查看三个命名空间

首先是java.*:当然,这些包构成了公共 API,但这只扩展到公共类的公共成员。可见度较低的类和成员是内部的,并且被模块系统强封装。

然后是sun.*。几乎所有此类包都是内部的,但有两个例外:sun.miscsun.reflect包由模块jdk.unsupported导出和打开,因为它们提供了对许多项目至关重要的功能,并且在 JDK 内部或外部没有可行的替代方案(最突出的是sun.misc.Unsafe)。不过,不要让这些非常具体的例外混淆更大的观点:一般来说,sun.*包应该被视为内部的,除了这两个包之外,实际上都是内部的。

最后是com.sun.*,它更复杂。整个命名空间是 JDK 特定的,这意味着它不是 Java 标准 API 的一部分,并且某些 JDK 可能不包含它。大约 90% 的它是未导出的包,它们是内部的。剩下的 10% 是由jdk.*模块导出的包,它们支持在 JDK 外部使用。这意味着它们在兼容性方面的演变与标准化 API 相似。 这里有一个内部包与导出包的列表

总之,使用java.*,避免sun.*,小心使用com.sun.*

强封装实验

为了体验强封装,让我们创建一个使用公共 API 中类的简单类

public class Internal {

    public static void main(String[] args) {
        System.out.println(java.util.List.class.getSimpleName());
    }

}

由于它是一个单一类,您可以直接运行它,无需显式编译

java Internal.java

这应该成功运行并打印“List”。

接下来,让我们混合使用那些出于兼容性原因可访问的例外情况之一

// add to `main` method
System.out.println(sun.misc.Unsafe.class.getSimpleName());

您仍然可以立即运行它,打印“List”和“Unsafe”。

现在让我们使用一个不可访问的内部类

// add to `main` method
System.out.println(sun.util.BuddhistCalendar.class.getSimpleName());

如果您尝试像以前一样运行它,您将收到编译错误(java 命令在内存中编译)

Internal.java:8: error: package sun.util is not visible
                System.out.println(sun.util.PreHashedMap.class.getSimpleName());
                                      ^
  (package sun.util is declared in module java.base, which does not export it)
1 error
error: compilation failed

错误消息非常清楚:包sun.util属于模块java.base,并且由于它没有导出它,因此它被认为是内部的,因此无法访问。

我们可以避免在编译期间使用该类型,而是使用反射

Class.forName("sun.util.BuddhistCalendar").getConstructor().newInstance();

执行该操作会导致运行时异常

Exception in thread "main" java.lang.IllegalAccessException:
    class Internal cannot access class sun.util.BuddhistCalendar (in module java.base)
    because module java.base does not export sun.util to unnamed module @1f021e6c
        at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:392)
        at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:674)
        at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:489)
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
        at org.codefx.lab.internal.Internal.main(Internal.java:9)

强封装在实践中

如果您绝对需要访问内部 API,有两个命令行标志可以让您绕过强封装

  • --add-exports 使导出包中的公共类型和成员在编译或运行时可访问
  • --add-opens 使打开包中的所有类型及其成员在运行时可用于反射

有关这两个选项以及如何使用它们的更多信息,请参阅本文

在编译期间应用--add-exports时,必须在运行应用程序时再次应用它,当然--add-opens只在运行时才有意义。这意味着无论哪段代码(您的代码或您的依赖项)需要访问 JDK 内部,都需要在启动应用程序时配置这些例外。这使应用程序所有者能够完全了解这些问题,并允许他们评估情况,要么更改代码/依赖项,要么明知故犯地接受使用内部 API 带来的可维护性风险。

强封装在所有显式模块周围都有效。这包括完全模块化的整个 JDK,但也可能包括您的代码和您的依赖项,如果它们是您放置在模块路径上的模块化 JAR。在这种情况下,前面所说的一切也适用于这些模块

  • 只有导出包中的公共类型和成员在编译和运行时可以在模块外部访问
  • 打开包中的所有类型和成员在运行时可以在模块外部访问
  • 其他类型和成员在编译期间和运行时都无法访问
  • 可以使用--add-exports(用于静态依赖项)和--add-opens(用于反射访问)创建例外

这意味着您可以将强封装的好处扩展到 JDK API 之外,以包括您的代码和您的依赖项。

强封装的演变

强封装是模块系统的基石,模块系统是在 Java 9 中引入的,但出于兼容性原因,类路径中的代码仍然可以访问 JDK 内部 API。这是通过命令行选项--illegal-access管理的,该选项在 JDK 9 到 15 中的默认值为permit。JDK 16 将该默认值更改为deny,而 17 则完全停用该选项。

从 17 开始,只有--add-exports--add-opens才能访问内部 API。


上次更新: 2021 年 9 月 14 日


系列中的上一篇
当前教程
强封装(JDK 内部)