JEP draft: Module Import Declarations (Second Preview)
Author | Jim Laskey & Gavin Bierman |
Owner | Gavin Bierman |
Type | Feature |
Scope | JDK |
Status | Submitted |
Component | specification / language |
Discussion | amber dash dev at openjdk dot org |
Relates to | JEP 476: Module Import Declarations (Preview) |
Reviewed by | Alex Buckley |
Created | 2024/07/09 09:53 |
Updated | 2024/10/08 17:17 |
Issue | 8335987 |
Summary
Enhance the Java programming language with the ability to succinctly import all of the packages exported by a module. This simplifies the reuse of modular libraries, but does not require the importing code to be in a module itself. This is a preview language feature.
History
Module import declarations were first proposed as a preview feature by JEP 476, and delivered in JDK 23. It is proposed to preview for a second time to gain more experience and feedback with the following additions:
-
Lift the current restriction that no module is able to declare a transitive dependence on the
java.base
module. Change the definition of thejava.se
module to transitively require thejava.base
module, meaning that importing thejava.se
module will now import on demand the entire Java SE API. -
Allow a type-import-on-demand declaration to shadow a module import declaration.
Goals
-
Simplify the reuse of modular libraries by allowing entire modules to be imported at once.
-
Avoid the noise of multiple type-import-on-demand declarations (e.g.,
import com.foo.bar.*
) when using diverse parts of the API exported by a module. -
Allow beginners to more easily use third-party libraries and fundamental Java classes without having to learn where they are located in a package hierarchy.
-
Module import declarations should work smoothly alongside existing import declarations.
-
Do not require developers who use the module import feature to modularize their own code.
Motivation
Classes and interfaces in the java.lang
package, such as Object
, String
,
and Comparable
, are essential to every Java program. For this reason, the Java
compiler automatically imports, on demand, all the classes and interfaces in the
java.lang
package, as if
import java.lang.*;
appears at the beginning of every source file.
As the Java Platform has evolved, classes and interfaces such as List
, Map
,
Stream
, and Path
have become almost as essential. However, none of these are
in java.lang
, so they are not automatically imported; rather, developers have
to keep the compiler happy by writing a plethora of import
declarations at the
beginning of every source file. For example, the following code converts an
array of strings into a map from capital letters to strings, but the imports
take almost as many lines as the code:
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()));
Developers have diverse views as to whether to prefer single-type-import or type-import-on-demand declarations. Many prefer single-type imports in large, mature codebases where clarity is paramount. However, in early-stage situations where convenience trumps clarity, developers often prefer on-demand imports; for example,
-
When prototyping code and running it with the
java
launcher; -
When exploring a new API in JShell, such as Stream Gatherers or the Foreign Function & Memory API; or
-
When learning to program with new features that work in concert with new APIs, such as virtual threads and their executors.
Since Java 9, modules have allowed a set of packages to be grouped together for reuse under a single name. The exported packages of a module are intended to form a cohesive and coherent API, so it would be convenient if developers could import on-demand from the entire module, that is, from all of the packages exported by the module. It would be as if all the exported packages are imported in one go.
For example, importing the java.base
module on-demand would give immediate
access to List
, Map
, Stream
, and Path
, without having to manually import
java.util
on-demand, and java.util.stream
on-demand, and java.nio.file
on-demand.
The ability to import at the level of modules would be especially helpful when
APIs in one module have a close relationship with APIs in another module. This
is common in large multi-module libraries such as the JDK. For example, the
java.sql
module provides database access via its java.sql
and javax.sql
packages, but
one of its interfaces,
java.sql.SQLXML
,
declares public
methods whose signatures use interfaces from the
javax.xml.transform
package in the
java.xml
module. Developers who call these methods in java.sql.SQLXML
typically import
both the java.sql
package and the javax.xml.transform
package. To
facilitate this extra import, the java.sql
module depends on the java.xml
module transitively, so
that a program which depends on the java.sql
module depends automatically on
the java.xml
module. In this scenario, it would be convenient if importing the
java.sql
module on-demand would also automatically import the java.xml
module on-demand. Automatically importing on-demand from transitive dependencies
would be a further convenience when prototyping and exploring.
Description
A module import declaration has the form
import module M;
It imports, on demand, all of the public
top-level classes and interfaces in
-
The packages exported by the module
M
to the current module, and -
The packages exported by the modules that are read by the current module due to reading the module
M
.
The second clause allows a program to use the API of a module, which might refer to classes and interfaces from other modules, without having to import all of those other modules.
For example:
-
import module java.base
has the same effect as 54 on-demand package imports, one for each of the packages exported by thejava.base
module. It is as if the source file containsimport java.io.*
andimport java.util.*
and so on. -
import module java.sql
has the same effect asimport java.sql.*
andimport javax.sql.*
plus on-demand package imports for the indirect exports of thejava.sql
module.
This is a preview language feature, disabled by default
To try the examples below in JDK 24, you must enable preview features:
-
Compile the program with
javac --release 24 --enable-preview Main.java
and run it withjava --enable-preview Main
; or, -
When using the source code launcher, run the program with
java --enable-preview Main.java
; or, -
When using
jshell
, start it withjshell --enable-preview
.
Syntax and semantics
We extend the grammar of import declarations
(JLS §7.5)
to include import module
clauses:
ImportDeclaration:
SingleTypeImportDeclaration
TypeImportOnDemandDeclaration
SingleStaticImportDeclaration
StaticImportOnDemandDeclaration
ModuleImportDeclaration
ModuleImportDeclaration:
import module ModuleName;
import module
takes a module name, so it is not possible to import packages
from the unnamed module, i.e., from the class path. This aligns with requires
clauses in module declarations, i.e., module-info.java
files, which take
module names and cannot express a dependence on the unnamed module.
import module
can be used in any source file. The source file need not be
associated with an explicit module. For example, java.base
and java.sql
are
part of the standard Java runtime, and can be imported by programs which are not
themselves developed as modules. (For technical background, see
JEP 261.)
import module
can be used in a source file that is associated with an explicit
module to conveniently import all the packages that are unqualified exports of
the module. In such a source file, packages in the module that are not exported
must continue to be imported in the traditional way. (Similarly for packages in
the module that are exported to other modules.) In other words, import module M
is not more powerful for code inside module M
than for code outside M
.
It is permitted to redundantly import the same module more than once in a source file.
Ambiguous imports
Since importing a module has the effect of importing multiple packages, it is possible to import classes with the same simple name from different packages. The simple name is ambiguous, so using it will cause a compile-time error.
For example, in this source file the simple name Element
is ambiguous:
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!
...
As another example, in this source file the simple name List
is ambiguous:
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!
...
As a final example, in this source file the simple name Date
is ambiguous:
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!
...
Ambiguities can be resolved straightforwardly by using another import declaration. Single-type-import declarations can shadow both type-import-on-demand and module import declarations; type-import-on-demand declarations can shadow module import declarations.
For example, to resolve the ambiguous Date
of the previous example, a
single-type-import declaration can be used:
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
...
In other cases, it is more convenient to use a type-import-on-demand declaration to resolve ambiguities:
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
...
This shadowing behavior allows developers to freely add module import declarations to working code without it breaking the code due to new ambiguities.
The shadowing behavior of the import declarations matches their specificity; the most specific (the single-type-import declaration) can shadow both lesser specific import declarations. A type-import-on-demand declaration can shadow a lesser specific module import declaration, but not the more specific single-type-import declaration.
Grouping import declarations
You may be able to coalesce multiple type-import-on-demand declarations into a single module import declaration, e.g.,
import javax.xml.*;
import javax.xml.parsers.*;
import javax.xml.stream.*;
could be coalesced and replaced with:
import module java.xml;
which is easier to read.
After this refactoring, it is possible that other type-import-on-demand declarations remain in the program. In this case, where a Java program has a mix of different import declarations, it may further aid reading to group these import declarations by their kind, e.g.,
// 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 { ... }
The order of these groups reflects their shadowing abilities as discussed earlier. So the single-type imports should come last, and the module imports first.
A worked example
Here is an example of how import module
works. Suppose C.java
is a source
file associated with module M0
:
// C.java
package q;
import module M1; // What does this import?
class C { ... }
where module M0
has the following declaration:
module M0 { requires M1; }
The meaning of import module M1
depends on the exports of M1
and any modules
that M1
requires transitively.
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; }
The effect of import module M1
is to
-
Import the
public
top level classes and interfaces from packagep1
, sinceM1
exportsp1
to everyone; -
Import the
public
top level classes and interfaces from packagep2
, sinceM1
exportsp2
toM0
, the module with whichC.java
is associated; and -
Import the
public
top level classes and interfaces from packagep10
, sinceM1
requires transitivelyM4
, which exportsp10
.
Nothing from packages p3
or p11
is imported by C.java
.
Importing modules in implicitly declared classes
This JEP is co-developed with the Implicitly Declared Classes and Instance
main
Methods JEP, which specifies that all public
top level classes and
interfaces in all packages exported by the java.base
module are automatically
imported on-demand in implicitly declared classes. In other words, it is as if
import module java.base
appears at the beginning of every such class, versus
import java.lang.*
at the beginning of every ordinary class. Note that an
implicitly declared class may still export import other modules, e.g.
java.desktop
, and may explicitly import the java.base
module even though
doing so is redundant.
The JShell tool automatically imports ten
packages on-demand. The list of packages is ad-hoc. We therefore propose to
change JShell to automatically import module java.base
.
Importing aggregator modules
It is sometimes useful to import an aggregator module, i.e. one which doesn't
have any direct exports, only indirect exports. For example, the
java.se
module does not export any packages, but it requires 19 other modules
transitively, so the effect of import module java.se
is to import the packages
which are exported by those modules, and so on, recursively — specifically, the
123 packages listed as the
indirect exports
of the java.se
module.
In earlier previews of this feature, when clients imported the java.se
module,
they were surprised to find that the java.base
module was not imported. This
meant that most clients had to import well known packages from java.base
by
hand, e.g., import java.util.*
, or alternatively import the java.base
module
itself.
The reason why importing the java.se
module did not import the java.base
module is that the java.se
module was unable to require the java.base
module
transitively. Java forbids any module from declaring a transitive dependence on
the java.base
module.
This restriction was sensible in the original design of Java modules, as all
modules have an implicit dependence on java.base
. But with the module import
feature, where module declarations are used to derive a set of packages to be
imported, the ability to require java.base
transitively is useful for
aggregator modules.
We now propose to lift this language restriction, and also to change the
java.se
module so that it transitively requires the java.base
module. This
will mean that a single import module java.se
is all that's needed to use the
entire Java SE API, no matter how many modules take part in exporting the API.
Only aggregator modules in the Java SE Platform should use requires transitive java.base
. The clients of such aggregators expect all java.*
modules to be
imported, including java.base
. Modules in the Java SE Platform that have both
direct exports and indirect exports are not, strictly speaking, aggregators;
they should not use requires transitive java.base
because it may pollute the
client's namespace. For example, the java.sql
module exports its own packages
as well as re-exporting packages from java.xml
and others, but a client that
says import module java.sql
is not necessarily interested in importing
everything from java.base
.
Note that the directive import module java.se
is only possible in a source
file associated with a named module that already requires java.se
. In a source
file associated with an unnamed module, such as one that implicitly declares a
class, it is not possible to use import module java.se
as java.se
is not in
the default set of root modules for an unnamed module.
Alternatives
-
An alternative to
import module ...
is to automatically import more packages than justjava.lang
. This would bring more classes into scope, i.e., usable by their simple names, and delay the need for beginners to learn about imports of any kind. But, which additional packages should we import automatically?Every reader will have suggestions for which packages to auto-import from the omnipresent
java.base
module:java.io
andjava.util
would be near-universal suggestions;java.util.stream
andjava.util.function
would be common; andjava.math
,java.net
, andjava.time
would each have supporters. For the JShell tool, we managed to find tenjava.*
packages which are broadly useful when experimenting with one-off Java code, but it is difficult to see which subset ofjava.*
packages deserves to be permanently and automatically imported into every Java program. The list would, moreover, change change as the Java Platform evolves; e.g.,java.util.stream
andjava.util.function
were introduced only in Java 8. Developers would likely become reliant on IDEs to remind them of which automatic imports are in effect — an undesirable outcome. -
An important use case for this feature is to automatically import on-demand from the
java.base
module in implicitly declared classes. This could alternatively be achieved by automatically importing the 54 packages exported byjava.base
. However, when an implicit class is migrated to an ordinary explicit class, which is the expected lifecycle, the developer would either have to write 54 on-demand package imports, or else figure out which imports are necessary.
Risks and Assumptions
Using one or more module import declarations leads to a risk of name ambiguity due to different packages declaring members with the same simple name. This ambiguity is not detected until the ambiguous simple name is used in a program, when a compile-time error will occur. The ambiguity can be resolved by adding a single-type-import declaration, but managing and resolving such name ambiguities could be burdensome and lead to code that is brittle and difficult to read and maintain.