跳到主要内容

JEP 476:模块导入声明(预览版)

概括

通过简洁地导入模块导出的所有包的能力来增强 Java 编程语言。这简化了模块化库的重用,但不需要导入代码位于模块本身中。这是预览语言功能

目标

  • 通过允许一次导入整个模块来简化模块化库的重用。

  • import com.foo.bar.*当使用模块导出的 API 的不同部分时,避免多个类型按需导入声明(例如,)的噪音。

  • 让初学者能够更轻松地使用第三方库和基本 Java 类,而无需了解它们在包层次结构中的位置。

  • 不要求使用模块导入功能的开发人员模块化自己的代码。

动机

包中的类和接口java.lang,例如ObjectString、 和Comparable,对于每个 Java 程序都是必不可少的。因此,Java 编译器会根据需要自动导入java.lang包中的所有类和接口,就好像

import java.lang.*;

出现在每个源文件的开头。

随着 Java 平台的发展,类和接口(例如ListMapStream和 )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 9 以来,模块允许将一组包组合在一起,以便在单个名称下重用。模块导出的包旨在形成一个内聚且连贯的 API,因此如果开发人员可以从整个模块(即从该模块导出的所有包)按需导入,将会很方便。就好像所有导出的包都一次性导入了一样。

例如,java.base按需导入模块将立即访问ListMapStreamPath,而无需手动java.util按需导入、java.util.stream按需导入和java.nio.file按需导入。

当一个模块中的 API 与另一模块中的 API 具有密切关系时,在模块级别导入的能力将特别有用。这在大型多模块库(例如 JDK)中很常见。例如,模块通过其和包java.sql提供数据库访问,但其接口之一声明了其签名使用模块中包中的接口的方法。调用这些方法的开发人员通常会同时导入包和包。为了方便这种额外的导入,模块可传递地依赖于该模块,以便依赖于该模块的程序自动依赖于该模块。在这种情况下,如果按需导入模块也能自动按需导入模块,那就方便了。在原型设计和探索时,自动按需从传递依赖项导入将更加方便。java.sql``javax.sqljava.sql.SQLXMLpublic``javax.xml.transformjava.xmljava.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.basejava.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

包中没有任何内容p3p11由 导入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.basejava.iojava.util将是近乎普遍的建议;java.util.stream并且java.util.function会很常见; 、java.mathjava.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 个按需包导入,要么弄清楚哪些导入是必要的。

风险和假设

由于不同的包使用相同的简单名称声明成员,因此使用一个或多个模块导入声明会导致名称模糊的风险。直到在程序中使用不明确的简单名称时才会检测到这种不明确性,此时会发生编译时错误。可以通过添加单一类型导入声明来解决歧义,但管理和解决此类名称歧义可能会很麻烦,并导致代码脆弱且难以阅读和维护。