JEP 476: Module Import Declarations (Preview) | 模块导入声明(预览)
摘要
通过能够简洁地导入模块导出的所有包来增强 Java 编程语言。这简化了模块化库的重用,但不要求导入代码本身处于模块中。这是一个 预览语言特性。
目标
- 通过允许一次导入整个模块来简化模块化库的重用。
- 在使用模块导出的 API 的不同部分时,避免多个按需类型导入声明(例如
import com.foo.bar.*
)的混乱。 - 让初学者更容易使用第三方库和基本的 Java 类,而无需了解它们在包层次结构中的位置。
- 不要求使用模块导入特性的开发人员将自己的代码模块化。
动机
java.lang
包中的类和接口,如 Object
、String
和 Comparable
,对每个 Java 程序都是必不可少的。出于这个原因,Java 编译器会自动按需导入 java.lang
包中的所有类和接口,就好像
import java.lang.*;
出现在每个源文件的开头一样。
随着 Java 平台的发展,诸如 List
、Map
、Stream
和 Path
等类和接口也变得几乎同样重要。然而,这些都不在 java.lang
中,所以它们不会自动导入;相反,开发人员必须在每个源文件的开头编写大量的 import
声明来让编译器满意。例如,以下代码将一个字符串数组转换为一个从大写字母到字符串的映射,但导入声明几乎和代码一样占行数:
import java.util.Map; // 或者 import java.util.*;
import java.util.function.Function; // 或者 import java.util.function.*;
import java.util.stream.Collectors; // 或者 import java.util.stream.*;
import java.util.stream.Stream; // (可以删除)
String[] fruits = new String[] { "apple", "berry", "citrus" };
Map<String, String> m =
Stream.of(fruits)
.collect(Collectors.toMap(s -> s.toUpperCase().substring(0,1),
Function.identity()));
开发人员对于是喜欢单一类型导入还是按需类型导入声明有不同的看法。在大型、成熟的代码库中,许多人 更喜欢 单一类型导入,在这种情况下清晰性至关重要。然而,在早期阶段,当便利性胜过清晰性时,开发人员通常更喜欢按需导入;例如:
- 当原型化代码并 使用
java
启动器运行它 时; - 在 JShell 中探索新的 API 时,如 流收集器 或 外部函数和内存 API;或者
- 当学习使用与新 API 协同工作的新特性进行编程时,如 虚拟线程及其执行器。
自 Java 9 以来,模块允许一组包在一个名称下组合在一起以供重用。模块的导出包旨在形成一个有凝聚力和连贯的 API,所以如果开发人员能够从整个模块按需导入,即从模块导出的所有包中按需导入,那将很方便。就好像所有导出的包一次性被导入一样。
例如,按需导入 java.base
模块将立即访问 List
、Map
、Stream
和 Path
,而无需手动按需导入 java.util
、按需导入 java.util.stream
和按需导入 java.nio.file
。
在模块级别进行导入的能力在一个模块中的 API 与另一个模块中的 API 有密切关系时特别有用。这在像 JDK 这样的大型多模块库中很常见。例如,java.sql
模块通过其 java.sql
和 javax.sql
包提供数据库访问,但其一个接口 java.sql.SQLXML
声明了 public
方法,这些方法的签名使用了 java.xml
模块中的 javax.xml.transform
包中的接口。在 java.sql.SQLXML
中调用这些方法的开发人员通常会导入 java.sql
包和 javax.xml.transform
包。为了方便这个额外的导入,java.sql
模块 传递性地 依赖于 java.xml
模块,因此依赖于 java.sql
模块的程序会自动依赖于 java.xml
模块。在这种情况下,如果按需导入 java.sql
模块也能自动按需导入 java.xml
模块,那将很方便。在原型化和探索时,自动从传递性依赖中按需导入将是一个进一步的便利。
描述
一个 模块导入声明 具有以下形式:
import module M;
它按需导入:
- 模块
M
向当前模块导出的包中的所有public
顶级类和接口;以及 - 由于读取模块
M
而被当前模块读取的模块所导出的包中的所有public
顶级类和接口。
第二个条款允许程序使用一个模块的 API,该 API 可能引用来自其他模块的类和接口,而无需导入所有那些其他模块。
例如:
import module java.base
具有与 54 个按需包导入相同的效果,对于java.base
模块导出的 每个包 都有一个。就好像源文件包含import java.io.*
和import java.util.*
等等。import module java.sql
具有与import java.sql.*
和import javax.sql.*
加上对java.sql
模块的 间接导出的按需包导入 相同的效果。
这是一个 预览语言特性,默认情况下禁用。
要在 JDK 23 中尝试下面的示例,必须启用预览特性:
- 使用
javac --release 23 --enable-preview Main.java
编译程序,并使用java --enable-preview Main
运行它;或者, - 当使用 源代码启动器 时,使用
java --enable-preview Main.java
运行程序;或者, - 当使用
jshell
时,使用jshell --enable-preview
启动它。
语法和语义
我们扩展了导入声明的语法(JLS §7.5)以包括 import module
子句:
ImportDeclaration:
SingleTypeImportDeclaration
TypeImportOnDemandDeclaration
SingleStaticImportDeclaration
StaticImportOnDemandDeclaration
ModuleImportDeclaration
ModuleImportDeclaration:
import module ModuleName;
import module
接受一个模块名,所以不可能从未命名模块(即类路径)中导入包。这与模块声明(即 module-info.java
文件)中的 requires
子句一致,后者接受模块名并且不能表达对未命名模块的依赖。
import module
可以在任何源文件中使用。源文件不需要与显式模块相关联。例如,java.base
和 java.sql
是标准 Java 运行时的一部分,可以被本身不是作为模块开发的程序导入。(有关技术背景,请参见 JEP 261。)
有时导入一个不导出任何包的模块是有用的,因为该模块传递性地需要其他导出包的模块。例如,java.se
模块(https://docs.oracle.com/en/java/javase/22/docs/api/java.se/module-summary.html)不导出任何包,但它传递性地需要其他 19 个模块,所以 import module java.se
的效果是导入那些模块导出的包,依此类推,递归地进行——具体来说,是作为 java.se
模块的 间接导出列出的 123 个包。请注意,只有在已经 requires java.se
的命名模块的编译单元中才可以使用 import module java.se
。在未命名模块的编译单元中,例如一个 隐式声明 一个类的编译单元中,不能使用 import module java.se
。
模糊导入
由于导入一个模块具有导入多个包的效果,所以有可能从不同的包中导入具有相同简单名称的类。简单名称是模糊的,所以使用它会导致编译时错误。
例如,在这个源文件中,简单名称 Element
是模糊的:
import module java.desktop; // 导出 javax.swing.text,
// 其中有一个公共的 Element 接口,
// 同时也导出 javax.swing.text.html.parser,
// 其中有一个公共的 Element 类
...
Element e =... // 错误 - 名称模糊!
...
作为另一个例子,在这个源文件中,简单名称 List
是模糊的:
import module java.base; // 导出 java.util,其中有一个公共的 List 接口
import module java.desktop; // 导出 java.awt,其中有一个公共的 List 类
...
List l =... // 错误 - 名称模糊!
...
作为最后一个例子,在这个源文件中,简单名称 Date
是模糊的:
import module java.base; // 导出 java.util,其中有一个公共的 Date 类
import module java.sql; // 导出 java.sql,其中有一个公共的 Date 类
...
Date d =... // 错误 - 名称模糊!
...
解决模糊性很简单:使用单一类型导入声明。例如,为了解决前面例子中模糊的 Date
:
import module java.base; // 导出 java.util,其中有一个公共的 Date 类
import module java.sql; // 导出 java.sql,其中有一个公共的 Date 类
import java.sql.Date; // 解决简单名称 Date 的模糊性!
...
Date d =... // 好!Date 被解析为 java.sql.Date
...
一个示例
这里是一个 import module
如何工作的例子。假设 C.java
是与模块 M0
相关联的源文件:
// C.java
package q;
import module M1; // 这个导入了什么?
class C {... }
其中模块 M0
有以下声明:
module M0 { requires M1; }
import module M1
的含义取决于 M1
的导出以及 M1
传递性地需要的任何模块。
module M1 {
exports p1;
exports p2 to M0;
exports p3 to M3;
requires transitive M4;
requires M5;
}
module M3 {... }
module M4 { exports p10; }
module M5 { exports p11; }
import module M1
的效果是:
- 从包
p1
中导入public
顶级类和接口,因为M1
向所有人导出p1
; - 从包
p2
中导入public
顶级类和接口,因为M1
向M0
(与C.java
相关联的模块)导出p2
;并且 - 从包
p10
中导入public
顶级类和接口,因为M1
传递性地需要M4
,而M4
导出p10
。
C.java
不会从包 p3
或 p11
中导入任何内容。
隐式声明的类
这个 JEP 与 “JEP 477:隐式声明的类和实例 main
方法” 共同开发,它指定在隐式声明的类中,java.base
模块导出的所有包中的所有 public
顶级类和接口将自动按需导入。换句话说,就好像每个这样的类的开头都有 import module java.base
,而普通类的开头是 import java.lang.*
。
JShell 工具自动按需导入十个包。包的列表是临时的。因此,我们建议将 JShell 改为自动 import module java.base
。
替代方案
import module...
的一个替代方案是自动导入比仅仅java.lang
更多的包。这将使更多的类进入作用域,即可以通过它们的简单名称使用,并延迟初学者学习任何类型导入的需求。但是,我们应该自动导入哪些额外的包呢?每个读者对于应该从无处不在的
java.base
模块中自动导入哪些包都有建议:java.io
和java.util
将是几乎普遍的建议;java.util.stream
和java.util.function
将很常见;而java.math
、java.net
和java.time
每个都有支持者。对于 JShell 工具,我们设法找到了十个java.*
包,在尝试一次性 Java 代码时非常有用,但很难确定java.*
包的哪个子集应该永久自动地导入到每个 Java 程序中。此外,随着 Java 平台的发展,这个列表会发生变化;例如,java.util.stream
和java.util.function
仅在 Java 8 中引入。开发人员可能会依赖于 IDE 来提醒他们哪些自动导入是有效的——这是一个不理想的结果。这个特性的一个重要用例是在隐式声明的类中自动从
java.base
模块按需导入。这也可以通过自动导入java.base
导出的 54 个包来实现。然而,当一个隐式类迁移到一个普通的显式类时(这是预期的生命周期),开发人员要么必须编写 54 个按需包导入,要么找出哪些导入是必要的。
风险和假设
使用一个或多个模块导入声明会由于不同的包声明具有相同简单名称的成员而导致名称模糊的风险。这种模糊性直到在程序中使用模糊的简单名称时才会被检测到,此时会发生编译时错误。模糊性可以通过添加单一类型导入声明来解决,但是管理和解决这样的名称模糊性可能很麻烦,并导致代码脆弱且难以阅读和维护。