JEP 476:模块导入声明(预览)
总结
增强 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; // or import java.util.*;
import java.util.function.Function; // or import java.util.function.*;
import java.util.stream.Collectors; // or import java.util.stream.*;
import java.util.stream.Stream; // (can be removed)
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 时,例如 Stream Gatherers 或 Foreign Function & Memory 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;
它按需导入所有 public
顶级类和接口。
-
模块
M
导出到当前模块的包,以及 -
由于读取模块
M
,当前模块读取的模块所导出的包。
第二条允许程序使用模块的 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 中尝试以下示例,您必须启用预览功能:
语法和语义
我们扩展了 import 声明的语法(JLS §7.5),以包含 import module
子句:
ImportDeclaration:
SingleTypeImportDeclaration
TypeImportOnDemandDeclaration
SingleStaticImportDeclaration
StaticImportOnDemandDeclaration
ModuleImportDeclaration
ModuleImportDeclaration:
import module ModuleName;
import module
需要一个模块名称,因此无法从未命名的模块(即类路径)中导入包。这与模块声明中的 requires
子句一致,即 module-info.java
文件,它们需要模块名称,且不能表达对未命名模块的依赖。
import module
可以在任何源文件中使用。该源文件不必与某个显式模块相关联。例如,java.base
和 java.sql
是标准 Java 运行时的一部分,可以被那些并非作为模块开发的程序导入。(有关技术背景,请参阅 JEP 261。)
有时,导入一个未导出任何包的模块也是有用的,因为该模块间接依赖于其他导出包的模块。例如,java.se
模块本身并未导出任何包,但它间接依赖 19 个其他模块,因此 import module java.se
的效果是导入那些模块所导出的包,并以此类推,递归进行 —— 具体来说,就是 java.se
模块的间接导出包列表 中列出的 123 个包。
模糊的导入
由于导入一个模块会产生导入多个包的效果,因此可能会从不同的包中导入具有相同简单名称的类。简单名称具有歧义,因此使用它会导致编译时错误。
例如,在此源文件中,简单名称 Element
是不明确的:
import module java.desktop; // exports javax.swing.text,
// which has a public Element interface,
// and also exports javax.swing.text.html.parser,
// which has a public Element class
...
Element e = ... // Error - Ambiguous name!
...
再比如,在这个源文件中,简单名称 List
是有歧义的:
import module java.base; // exports java.util, which has a public List interface
import module java.desktop; // exports java.awt, which a public List class
...
List l = ... // Error - Ambiguous name!
...
最后一个例子,在这个源文件中,简单名称 Date
是有歧义的:
import module java.base; // exports java.util, which has a public Date class
import module java.sql; // exports java.sql, which has a public Date class
...
Date d = ... // Error - Ambiguous name!
...
解决歧义的方法很简单:使用单类型导入声明。例如,要解决上一个例子中 Date
的歧义,可以这样做:
import module java.base; // exports java.util, which has a public Date class
import module java.sql; // exports java.sql, which has a public Date class
import java.sql.Date; // resolve the ambiguity of the simple name Date!
...
Date d = ... // Ok! Date is resolved to java.sql.Date
...
一个详细示例
下面是 import module
工作原理的一个示例。假设 C.java
是与模块 M0
关联的源文件:
// C.java
package q;
import module M1; // What does this import?
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
将p2
导出给M0
,而C.java
与模块M0
相关联;以及 -
从包
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 个按需包导入,要么弄清楚哪些导入是必要的。
风险与假设
使用一个或多个模块导入声明会导致名称歧义的风险,因为不同的包可能会声明具有相同简单名称的成员。这种歧义在程序中使用了有歧义的简单名称时才会被发现,并且会导致编译时错误。可以通过添加单类型导入声明来解决这种歧义,但管理和解决这些名称歧义可能会带来负担,并导致代码变得脆弱、难以阅读和维护。