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
| Release | Status | Spec |
|---|---|---|
| Java 23 | Preview | JEP 476 |
| Java 24 | Second preview | JEP 494 |
| Java 25 LTS | Final | JEP 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:
- The packages module
Mexports to the current module, and - The packages exported by the modules that become readable because you read
M— that is,M’srequires transitivechain.
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:
| Property | Behaviour |
|---|---|
| Argument | A module name, never a package name |
| Class path types | Not importable — the unnamed module has no name to write |
| Where it is legal | Any source file, module or no module |
Inside module M itself | Same rules — qualified and non-exported packages still need normal imports |
| Repeats | Importing 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
Lab: coalesce related package imports
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 name | Colliding types | Triggered by |
|---|---|---|
List | java.util.List / java.awt.List | java.base + java.desktop |
Date | java.util.Date / java.sql.Date | java.base + java.sql |
Element | javax.swing.text.Element / javax.swing.text.html.parser.Element | java.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.
| Context | Implicit import |
|---|---|
| Compact source files | import module java.base; |
| JShell | import 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 modulein 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 Mbefore 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 moduleto reach class-path types; it takes module names only. - Assume
import module java.seworks 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 MinsideMas access to its non-exported packages. - Forget the implicit
java.baseimport 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.