JEP 494:模块导入声明(第二预览版)
概括
通过简洁地导入模块导出的所有包来增强 Java 编程语言。这简化了模块库的重用,但不需要导入代码位于模块本身中。这是一项预览语言功能。
历史
模块导入声明最初是由JEP 476 (JDK 23)作为预览功能提出的。我们在此建议对其进行第二次预览,以获得更多经验和反馈,并添加两项内容:
-
取消任何模块都不能声明对
java.base
模块的传递依赖的限制,并修改模块的声明java.se
以传递地需要java.base
模块。通过这些更改,导入java.se
模块将根据需要导入整个 Java SE API。 -
允许按需类型导入声明遮蔽模块导入声明。
目标
-
通过允许一次导入整个模块来简化模块库的重用。
-
import com.foo.bar.*
当使用模块导出的 API 的不同部分时,避免多个按需类型导入声明(例如)的噪音。 -
让初学者更容易使用第三方库和基本 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()));
对于是选择单类型导入声明还是按需类型导入声明,开发人员持有不同的看法。许多人更喜欢在大型、成熟的代码库中使用单类型导入,因为清晰度是最重要的。然而,在早期阶段,当便利性胜过清晰度时,开发人员通常更喜欢按需导入;例如,
-
在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
方法,这些方法的签名使用javax.xml.transform
模块中包中的接口java.xml
。在 中调用这些方法的开发人员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,该 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 24 中尝试以下示例,您必须启用预览功能:
-
使用 编译程序
javac --release 24 --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
采用模块名称,因此无法从未命名的模块(即从类路径)导入包。这与requires
模块声明中的子句(即module-info.java
文件)一致,这些子句采用模块名称,但无法表达对未命名模块的依赖。
import module
可以在任何源文件中使用,无论该文件是否是显式模块定义的一部分。例如,java.base
和java.sql
是标准 Java 运行时的一部分,并且可以由仅部署在类路径上的类导入。(有关技术背景,请参阅JEP 261。)
在作为显式模块定义一部分的源文件中,import module
可用于方便地导入该模块无条件导出的所有包。在这样的源文件中,模块中未导出或有条件导出的包必须继续以传统方式导入。(换句话说,对于模块内部代码而言,其功能并不比外部代码import module M
强大。)M``M
一个源文件可能会多次导入同一个模块。
解决歧义导入
导入模块相当于导入多个包,因此可以从不同的包导入具有相同简单名称的类。简单名称具有歧义,因此使用它会导致编译时错误。
例如,在此源文件中,简单名称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
上一个示例中的歧义:Date``import module
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 java.base;
import module java.desktop;
import java.util.*;
import javax.swing.text.*;
...
Element e = ... // Element is resolved to javax.swing.text.Element
List l = ... // List is resolved to java.util.List
Document d = ... // Document is resolved to javax.swing.text.Document,
// regardless of any module imports
...
导入声明的隐藏行为与其特殊性相匹配。最特殊的,即单类型导入声明,可以隐藏按需导入声明和模块导入声明,后者的特殊性较低。按需导入声明可以隐藏模块导入声明,后者的特殊性较低,但不能隐藏单类型导入声明,后者的特殊性较高。
合并导入声明
您可以将多个按需声明合并为一个模块导入声明;例如:
import javax.xml.*;
import javax.xml.parsers.*;
import javax.xml.stream.*;
可以替换为:
import module java.xml;
更加容易阅读。
分组导入声明
如果源文件混合了不同类型的导入声明,那么按类型分组可以进一步提高可读性;例如:
// Module imports
import module M1;
import module M2;
...
// Package imports
import P1.*;
import P2.*;
...
// Single-type imports
import P1.C1;
import P2.C2;
...
class Foo { ... }
组的顺序反映了它们的阴影行为:最不具体的模块导入声明在第一个,最具体的单一类型导入在最后,按需导入在中间。
一个可行的例子
下面是一个如何工作的例子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
是
-
从包中导入
public
顶级类和接口p1
,因为M1
导出p1
给所有人; -
从包中导入
public
顶级类和接口p2
,因为M1
导出p2
到M0
与之关联的模块C.java
;并且 -
public
从包中导入顶级类和接口p10
,因为M1
需要传递M4
,然后导出p10
。
包p3
或中没有任何p11
内容被导入C.java
。
在简单源文件中导入模块
此 JEP 与 JEP简单源文件和实例main
方法共同开发,后者规定模块public
导出的每个包中的每个顶级类和接口java.base
都会在简单源文件中按需自动导入。换句话说,它就像import module java.base
出现在每个此类文件的开头一样。简单源文件可以导入其他模块,例如,java.desktop
并且可以显式导入java.base
模块,即使这样做是多余的。
JShell工具会根据需要自动导入 10 个包。包列表是临时的。因此我们建议将 JShell 更改为自动import module java.base
。
导入聚合器模块
有时导入_聚合模块_很有用,即模块本身不导出任何包,但会导出其所需模块导出的包。例如,模块不导出java.se
任何包,但它需要其他十九个模块,因此的效果是导入这些模块导出的包,依此类推,递归 — 具体来说,是列出的模块间接导出的123 个包。import module java.se``java.se
在这个功能的早期预览版中,开发人员惊讶地发现导入模块java.se
并没有起到导入模块的效果java.base
。因此他们必须同时导入 java.base 模块,或者从 导入特定的包java.base
,例如import java.util.*
。
导入java.se
模块不会导入java.base
模块,因为 Java 语言规范明确禁止任何模块声明对java.base
模块的传递依赖。此限制在模块功能的原始设计中是合理的,因为每个模块都隐式依赖java.base
。但是,对于使用模块声明来派生一组要导入的包的模块导入功能,java.base
传递依赖的能力很有用。
因此,我们建议取消这一语言限制。我们还将修改模块的声明java.se
,使其可传递地需要该模块。因此,无论有多少模块参与导出 API,只需java.base
一个即可使用整个标准 Java API。import module java.se
只有 Java 平台中的聚合器模块才应使用requires transitive java.base
。此类聚合器的客户端希望java.*
导入所有模块,包括java.base
。严格来说,Java 平台中具有直接导出和间接导出的模块不是聚合器。因此,它们不应使用,requires transitive java.base
因为它可能会污染客户端的命名空间。例如,java.sql
模块导出自己的包以及来自java.xml
和其他包的包,但表示的客户端import module java.sql
不一定有兴趣从 导入所有内容java.base
。
该指令import module java.se
仅在属于已 的显式模块定义的源文件中有效requires java.se
。在模块定义之外的源文件中,尤其是在隐式声明类的简单源文件中,使用import module java.se
会失败,因为java.se
不在未命名模块的默认根模块集中。在属于自动模块定义的源文件中,import module java.se
如果其他已解析的显式模块 ,则将起作用requires java.se
。
替代方案
-
的替代方法
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
。这也可以通 过自动导入 导出的 54 个包来实现java.base
。但是,当隐式类迁移到普通显式类(这是预期的生命周期)时,开发人员要么必须编写 54 个按需包导入,要么弄清楚哪些导入是必要的。
风险和假设
使用一个或多个模块导入声明会导致名称歧义的风险,因为不同的软件包使用相同的简单名称声明成员。只有在程序中使用了歧义的简单名称时才会检测到这种歧义,此时将发生编译时错误。可以通过添加单类型导入声明来解决这种歧义,但管理和解决此类名称歧义可能会很麻烦,并导致代码脆弱且难以阅读和维护。