JEP 476: Module Import Declarations (Preview)

AuthorJim Laskey & Gavin Bierman
OwnerGavin Bierman
TypeFeature
ScopeSE
StatusIntegrated
Release23
Componentspecification / language
Discussionamber dash dev at openjdk dot org
EffortS
DurationS
Relates toJEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)
Reviewed byAlex Buckley, Brian Goetz
Endorsed byBrian Goetz
Created2023/08/28 16:57
Updated2024/05/07 17:52
Issue8315129

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.

Goals

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,

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 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:

This is a preview language feature, disabled by default

To try the examples below in JDK 23, you must enable preview features:

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.)

It is sometimes useful to import a module that does not export any packages, because the module transitively requires other modules that do export packages. 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.

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!
...

Resolving ambiguities is straightforward: Use a single-type-import declaration. For example, to resolve the ambiguous Date of the previous example:

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
...

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

Nothing from packages p3 or p11 is imported by C.java.

Implicitly declared classes

This JEP is co-developed with JEP 477: Implicitly Declared Classes and Instance main Methods, 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.

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.

Alternatives

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.