Open a scratch file, reach for Stream, Collectors, and Function, and you spend the first four lines telling the compiler where the JDK keeps its own types.

import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

String[] fruits = { "apple", "berry", "citrus" };
Map<String, String> m = Stream.of(fruits)
        .collect(Collectors.toMap(s -> s.toUpperCase().substring(0, 1),
                                  Function.identity()));

The imports are as long as the code. Java 25 gives you a wider unit to import from — a whole module:

import module java.base;

String[] fruits = { "apple", "berry", "citrus" };
Map<String, String> m = Stream.of(fruits)
        .collect(Collectors.toMap(s -> s.toUpperCase().substring(0, 1),
                                  Function.identity()));

This is Module Import Declarations, finalized by JEP 511. It is a language feature about simple names — you do not have to modularize your own code to use it. import module is that language convenience. JPMS and module-info.java are a different post: Java Modules: What module-info.java Actually Buys You.

When it shipped

ReleaseStatusSpec
Java 23PreviewJEP 476
Java 24Second previewJEP 494
Java 25 LTSFinalJEP 511

No --enable-preview on Java 25. The second preview is where java.se gained its transitive dependence on java.base, so behavior you read about from the Java 23 preview may be stale.

Mental model: one declaration, many on-demand imports

import module M; is shorthand for a set of import P.*; declarations. It imports every public top-level class and interface in:

  1. The packages module M exports to the current module, and
  2. The packages exported by the modules that become readable because you read M — that is, M’s requires transitive chain.

Rule 2 is the part that saves real typing. java.sql exports only java.sql and javax.sql, but java.sql.SQLXML hands you javax.xml.transform types, so java.sql requires java.xml transitively:

import module java.sql; // java.sql, javax.sql, plus java.xml's exports, plus java.logging's

SQLXML xml = resultSet.getSQLXML("payload");
Source source = xml.getSource(null); // javax.xml.transform.Source — no extra import

A few properties are worth memorizing:

PropertyBehaviour
ArgumentA module name, never a package name
Class path typesNot importable — the unnamed module has no name to write
Where it is legalAny source file, module or no module
Inside module M itselfSame rules — qualified and non-exported packages still need normal imports
RepeatsImporting the same module twice is legal and harmless

That fourth row surprises people. import module M inside M is not a private back door: packages M does not export, or exports only to a named module, still need a java.util.Whatever-style import.

Lab: collapse an import wall

Take a file that pulls from four packages of the same module. On Java 25, one line replaces all of them.

import module java.base;

class FruitIndex {
    Map<String, String> byInitial(String... fruits) {
        return Stream.of(fruits)
                .collect(Collectors.toMap(s -> s.toUpperCase().substring(0, 1),
                                          Function.identity()));
    }

    void main() {
        IO.println(byInitial("apple", "berry", "citrus"));
    }
}

Nothing about the build changes — this is an ordinary class, compiled the ordinary way:

javac FruitIndex.java && java FruitIndex
{A=apple, B=berry, C=citrus}

import module java.base stands in for 54 on-demand package imports, one per unqualified export of java.base. You can confirm that number on your own JDK:

java --describe-module java.base | grep -E '^exports ' | grep -v ' to ' | wc -l
54

The same trick works for the smaller platform modules, where three or four import X.*; lines usually travel together. XML parsing is the classic case:

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

All three packages come from one module, so one declaration covers them:

import module java.xml;

Before you trust a module import, list what it actually brings in. --describe-module is the honest answer to “what does this import give me?”:

java --describe-module java.xml
java.xml@25
exports javax.xml
exports javax.xml.catalog
exports javax.xml.datatype
exports javax.xml.namespace
exports javax.xml.parsers
...

Lab: the whole SE API with java.se

java.se is an aggregator: it exports nothing itself and instead requires nineteen modules transitively. Rule 2 does the rest, which is why one line reaches the whole standard API.

import module java.se;

class Report {
    void main() {
        Path out = Path.of("report.txt");            // java.base
        Logger log = Logger.getLogger("report");     // java.logging
        log.info(out.toAbsolutePath().toString());
        IO.println(DriverManager.getDrivers().hasMoreElements()); // java.sql
    }
}

java.base is in that list only because Java 24 lifted an old restriction. The Java Language Specification used to forbid requires transitive java.base, so on Java 23 import module java.se left List and Map out of scope and people had to import java.base as well. Java 24 and 25 declare java.se with requires transitive java.base, so one import module java.se covers the SE API.

There is a catch, and it has nothing to do with the language. java.se is not in the default set of root modules for the unnamed module, so a class-path file that imports it fails to compile:

Report.java:1: error: module not found: java.se
import module java.se;
              ^
1 error

You have two honest fixes. Either make java.se a root module explicitly:

javac --add-modules java.se Report.java
java --add-modules java.se Report

Or use it from a source file that belongs to an explicit module which already requires it:

module com.geekmonks.report {
    requires java.se;
}

Note: The same gap hits compact source files — they live in the unnamed module, so import module java.se fails there too while import module java.base and import module java.desktop work fine.

Gotcha: two modules, one simple name

A wider import means more chances that two packages ship the same simple name. The compiler does not complain at the import; it complains the first time you use the ambiguous name.

import module java.base;    // exports java.util — interface List
import module java.desktop; // exports java.awt  — class List

class Widgets {
    void main() {
        List items = null; // which List?
    }
}
Widgets.java:6: error: reference to List is ambiguous
        List items = null;
        ^
  both interface java.util.List in java.util and class java.awt.List in java.awt match
1 error

The fix is one more import declaration. A single-type import is the most specific form, so it shadows both module imports:

import module java.base;
import module java.desktop;

import java.util.List; // resolve the ambiguity

class Widgets {
    void main() {
        List<String> items = List.of("ok", "now unambiguous");
        IO.println(items);
    }
}

List is not the only collision you will meet. Keep these three in mind:

Simple nameColliding typesTriggered by
Listjava.util.List / java.awt.Listjava.base + java.desktop
Datejava.util.Date / java.sql.Datejava.base + java.sql
Elementjavax.swing.text.Element / javax.swing.text.html.parser.Elementjava.desktop alone

Element is the instructive one: a single module import can be ambiguous with itself, because one module can export two packages that both declare the name.

Shadowing follows specificity

When the same simple name arrives from several places, the more specific declaration wins. There are three tiers:

import java.util.List;     single-type  — most specific, shadows everything below
import java.util.*;        on-demand    — shadows module imports, loses to single-type
import module java.base;   module       — least specific

That gives you a second tool for ambiguity: when a whole package should win, shadow with an on-demand import instead of listing types one by one.

import module java.base;
import module java.desktop;

import java.util.*;        // java.util wins for List
import javax.swing.text.*; // javax.swing.text wins for Element and Document

Because the tiers are ordered, grouping your imports in that order reads the way the compiler resolves them:

// Module imports — least specific
import module java.base;
import module java.desktop;

// Package imports
import java.util.*;

// Single-type imports — most specific
import javax.swing.text.Element;

Where import module already happens for you

Two places apply this feature without you writing it. Neither is optional, and both are worth knowing so the behavior does not look like magic.

ContextImplicit import
Compact source filesimport module java.base;
JShellimport module java.base; (replaces its old ad-hoc list of ten packages)

A compact file may still import other modules, and may repeat java.base harmlessly:

import module java.desktop;

void main() {
    var sizes = List.of(12, 14, 18);   // java.util, implicit
    var accent = new Color(0, 122, 204); // java.awt, from java.desktop
    IO.println(sizes + " " + accent);
}

The implicit import disappears when you wrap the file in a class. Add import module java.base; in the same edit, or the file that compiled a second ago stops compiling.

Cheat sheet

Java 25 / JEP 511: final, no preview flag

import module M;         // module name, not a package name

Imports on demand:
  - public top-level types in packages M exports to YOUR module
  - plus packages exported via M's `requires transitive` chain

import module java.base    == 54 on-demand package imports
import module java.se      == the whole SE API (aggregator, 19 transitive requires)
                              needs java.se as a root module:
                              javac --add-modules java.se ...
                              or a module that `requires java.se`

Cannot import: the unnamed module (class-path types have no module name)
Legal anywhere: modular or non-modular source; duplicates allowed
Inside module M: still no access to unexported / qualified-export packages

Ambiguity is reported at USE, not at import:
  error: reference to List is ambiguous
Fix by specificity:
  import java.util.List;   single-type  (most specific)
  import java.util.*;      on-demand
  import module java.base; module       (least specific)

Implicit already: compact source files, JShell -> import module java.base

Do:

  • Reach for import module in scripts, JShell sessions, katas, and spikes.
  • Coalesce package imports that all come from one module (javax.xml.* → import module java.xml).
  • Run java --describe-module M before trusting what a module import brings in.
  • Resolve a clash with the narrowest declaration that fixes it — usually a single-type import.
  • Group imports module → on-demand → single-type so the file reads in shadowing order.

Don’t:

  • Expect import module to reach class-path types; it takes module names only.
  • Assume import module java.se works from a class-path file without --add-modules java.se.
  • Stack many module imports in a large codebase — every one widens the ambiguity surface.
  • Treat import module M inside M as access to its non-exported packages.
  • Forget the implicit java.base import when promoting a compact file to a class.

Wrap-up

Module import declarations trade precision for reach. import module java.base puts List, Map, Stream, and Path in simple-name scope in one line; import module java.se does the same for the whole SE API once java.se is a root module. The cost is ambiguity you find at the use site, and single-type imports are the cure.

Use it where convenience beats clarity — one-file programs, JShell, and API exploration — and keep single-type imports in the mature code that other people read. If you want the version of this feature you never have to type, compact source files already start every file with import module java.base.