JEP 476:模块导入声明(预览版)
概括
通过简洁地导 入模块导出的所有包的能力来增强 Java 编程语言。这简化了模块化库的重用,但不需要导入代码位于模块本身中。这是预览语言功能。
目标
-
通过允许一次导入整个模块来简化模块化库的重用。
-
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或外部函数和内存 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,该模块可能引用其他模块的类和接口,而无需导入所有其他模块。
例如:
-
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
采用模块名称,因此无法从未命名的模块(即从类路径)导入包。这与requires
模块声明(即文件)中的子句一致,module-info.java
这些子句采用模块名称并且不能表达对未命名模块的依赖。
import module
可以在任何源文件中使用。源文件不需要与显式模块关联。例如,java.base
和java.sql
是标准 Java 运行时的一部分,并且可以由本身不是作为模块开发的程序导入。 (有关技术背景,请参阅JEP 261。)
有时导入不导出任何包的模块很有用,因为该模块传递地需要其他导出包的模块。例如,该java.se
模块不导出任何包,但它需要传递其他 19 个模块,因此 的效果import module java.se
是递归地导入这些模块导出的包,依此类推 - 具体来说,即列出为间接的123 个包java.se
模块的导出。
含糊不清的进口
由于导入模块具有导入多个包的效果,因此可以从不同的包导入具有相同简单名称的类。简单的名称是不明确的,因此使用它会导致编译时错误。
例如,在此源文件中,简单名称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
是与 module 关联的源文件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
是
-
从 package导入
public
顶级类和接口p1
,因为M1
导出p1
到每个人; -
public
从 package导入顶级类和接口p2
,因为M1
导出p2
到与其关联的M0
模块;C.java
和 -
从包中导入
public
顶级类和接口p10
,因为M1
需要传递性地M4
导出p10
。
包中没有任何内容p3
或p11
由 导入C.java
。
隐式声明的类
此 JEP 与JEP 477:隐式声明的类和实例main
方法共同开发,该 JEP 指定模块public
导出的所有包中的所有顶级类和接口java.base
都会在隐式声明的类中按需自动导入。换句话说,它似乎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 工具,我们设法找到了 10 个java.*
包,这些包在试验一次性 Java 代码时广泛有用,但很难看出哪些java.*
包子集值得永久且自动导入到每个 Java 程序中。此外,随着 Java 平台的发展,该列表也会发生变化。java.util.stream
例如,java.util.function
仅在 Java 8 中引入。开发人员可能会依赖 IDE 来提醒他们哪些自动导入正在生效——这是一个不希望的结果。 -
此功能的一个重要用例是自动按需
java.base
从隐式声明的类中的模块导入。这也可以通过自动导入 . 导出的 54 个包来实现java.base
。然而,当隐式类迁移到普通显式类(这是预期的生命周期)时,开发人员要么必须编写 54 个按需包导入,要么弄清楚哪些导入是必要的。
风险和假设
由于不同的包使用相同的简单名称声明成员,因此使用一个或多个模块导入声明会导致名称模糊的风险。直到在程序中使用不明确的简单名称时才会检测到这种不明确性,此时会发生编译时错误。可以通过添加单一类型导入声明来解决歧义,但管理和解决此类名称歧义可能会很麻烦,并导致代码脆弱且难以阅读和维护。