Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8335987

JEP 494: Module Import Declarations (Second Preview)

    • Jim Laskey & Gavin Bierman
    • Feature
    • Open
    • SE
    • amber dash dev at openjdk dot org
    • 494

      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 (JDK 23). We here propose to preview them for a second time to gain more experience and feedback, with two additions:

      • Lift the restriction that no module is able to declare a transitive dependence on the java.base module, and revise the declaration of the java.se module to transitively require the java.base module. With these changes, importing the java.se module will import the entire Java SE API on demand.

      • Allow type-import-on-demand declarations to shadow module import declarations.

      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.

      • Ensure that module import declarations 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 of 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 <code class="prettyprint" data-shared-secret="1731389207507-0.8802033004726278">java.sql</code> module provides database access via its java.sql and javax.sql packages, but one of its interfaces, <code class="prettyprint" data-shared-secret="1731389207507-0.8802033004726278">java.sql.SQLXML</code>, declares public methods whose signatures use interfaces from the javax.xml.transform package in the <code class="prettyprint" data-shared-secret="1731389207507-0.8802033004726278">java.xml</code> 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:

      This is a preview language feature, disabled by default

      To try the examples below in JDK 24, 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, whether or not that file is part of the definition of an explicit module. For example, java.base and java.sql are part of the standard Java runtime, and can be imported by classes which are only ever deployed on the class path. (For technical background, see JEP 261.)

      Within a source file that is part of the definition of an explicit module, import module can be used to conveniently import all of the packages that are exported, without qualification, by that module. In such a source file, packages in the module that are either not exported or exported with qualification must continue to be imported in the traditional way. (In other words, import module M is not more powerful for code inside module M than for code outside M.)

      A source file may import the same module more than once.

      Resolving ambiguous imports

      Importing a module has the effect of importing multiple packages, so 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 another import declaration. For example, adding a single-type-import declaration resolves the ambiguous Date of the previous example by shadowing the Date classes imported by the import module declarations:

      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 add an on-demand declaration to resolve ambiguities by shadowing all of the classes in a package:

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

      The shadowing behavior of import declarations matches their specificity. The most specific, i.e., single-type-import declarations, can shadow both on-demand and module import declarations, which are less specific. On-demand declarations can shadow module import declarations, which are less specific, but not single-type-import declarations, which are more specific.

      Coalescing import declarations

      You may be able to coalesce multiple on-demand declarations into a single module import declaration; for example:

      import javax.xml.*;
      import javax.xml.parsers.*;
      import javax.xml.stream.*;

      could be replaced with:

      import module java.xml;

      which is easier to read.

      Grouping import declarations

      If a source file has a mix of different kinds of import declarations then grouping them by kind may further improve readability; for example:

      // 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 the groups reflects their shadowing behavior: The least-specific module import declarations are first, the most-specific single-type imports are last, and the on-demand imports are in between.

      A worked example

      Here is an example of how import module works. Suppose the source file C.java is part of the definition of 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 package p1, since M1 exports p1 to everyone;

      • Import the public top level classes and interfaces from package p2, since M1 exports p2 to M0, the module with which C.java is associated; and

      • Import the public top level classes and interfaces from package p10, since M1 requires transitively M4, which exports p10.

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

      Importing modules in simple source files

      This JEP is co-developed with the JEP

      <em>Simple Source Files and Instance <code class="prettyprint" data-shared-secret="1731389207507-0.8802033004726278">main</code><br /> Methods</em>

      , which specifies that every public top level class and interface in every package exported by the java.base module is automatically imported on-demand in a simple source file. In other words, it is as if import module java.base appears at the beginning of every such file. A simple source file may 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., a module which does not export any packages itself but does export the packages exported by the modules it requires. For example, the <code class="prettyprint" data-shared-secret="1731389207507-0.8802033004726278">java.se</code> module does not export any packages, but it requires nineteen other modules transitively, so the effect of import module java.se is to import the packages 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, developers were surprised to see that importing the java.se module did not have the effect of importing the java.base module. They thus had to either import the java.base module as well, or else import specific packages from java.base, e.g., import java.util.*.

      Importing the java.se module did not import the java.base module because the Java Language Specification

      explicitly<br /> forbade

      any module from declaring a transitive dependence on the java.base module. This restriction was sensible in the original design of the modules feature, since every module has an implicit dependence on java.base. With the module import feature, however, which uses module declarations to derive a set of packages to be imported, the ability to require java.base transitively is useful.

      We therefore propose to lift this language restriction. We will also revise the declaration of the java.se module so that it transitively requires the java.base module. Thus a single import module java.se is all that is needed to use the entire standard Java API, no matter how many modules take part in exporting the API.

      Only aggregator modules in the Java 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 Platform that have both direct exports and indirect exports are not, strictly speaking, aggregators. Hence 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 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.

      The directive import module java.se works only in source files that are part of the definition of an explicit module that already requires java.se. In a source file outside of a module definition, and in particular in a simple source file which implicitly declares a class, using import module java.se fails because java.se is not in the default set of root modules for the unnamed module. In a source file that is part of the definition of an automatic module, import module java.se will work if some other resolved explicit module requires java.se.

      Alternatives

      • An alternative to import module ... is to automatically import more packages than just java.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 and java.util would be near-universal suggestions; java.util.stream and java.util.function would be common; and java.math, java.net, and java.time would each have supporters. For the JShell tool, we managed to find ten java.* packages which are broadly useful when experimenting with one-off Java code, but it is difficult to see which subset of java.* 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 and java.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 by java.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.

            jlahoda Jan Lahoda
            gbierman Gavin Bierman
            Gavin Bierman Gavin Bierman
            Alex Buckley, Brian Goetz
            Brian Goetz
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated: