java.util.List, java.util.Map, java.util.stream.Collectors, java.nio.file.Path, and so on, most of them added by an IDE nobody looks at again. JEP 511, final in Java 25, adds a one-line alternative: import module java.base; makes every public class and interface in every package that module exports available by its simple name.
This article shows what that line does, what it costs you (nothing at runtime, but one new way to get an ambiguity error), which imports win when names collide, the two surprises I hit — import module java.sql; does not import java.base, and import module java.se; does not compile without an extra flag — and where the feature makes sense (scripts, teaching, compact source files) versus where a long explicit import list is still the better choice. Each claim comes from a real compile and run, and each code block links to its file in the companion repository.
Versions. Tested on JDK 25.0.4.1 (Temurin, LTS) and JDK 27+35 (Temurin, GA 15 September 2026). JDK 21 appears only for the “before” transcripts. JEP 511 is final in 25: no--enable-preview. Compiling this syntax with--release 24or lower is rejected (the exact message is in the accordion at the end of the first section).openjdk.org/jepsreturned HTTP 403 to the tooling used for this article, so behaviour below comes fromjavac,javaand the Java Language Specification, section 7.5.5, not from the JEP text.
One line instead of seven: what import module does
Here is a small program written the way Java has always asked for it. It groups words by first letter and prints a path and a date, so it needs collections, streams, Path and LocalDate. That is seven imports, two of which (Function and Stream) the code never uses, which is exactly how import lists grow in real files:
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class OldWay {
public static void main(String[] args) {
List<String> words = List.of("apple", "avocado", "banana", "blueberry", "cherry");
Map<Character, List<String>> byInitial = words.stream()
.collect(Collectors.groupingBy(w -> w.charAt(0)));
System.out.println(byInitial);
System.out.println(Path.of("a", "b", "c.txt") + " on " + LocalDate.of(2026, 9, 15));
}
}
Source: OldWay.java. Now the same program with the seven lines replaced by one. Nothing else in the file changes:
import module java.base;
Source: NewWay.java (everything after that line is identical to OldWay’s body). Both compile and print the same thing on both JDKs:
$ javac src/OldWay.java && java OldWay (25.0.4.1+1-LTS)
{a=[apple, avocado], b=[banana, blueberry], c=[cherry]}
a/b/c.txt on 2026-09-15
$ javac src/NewWay.java && java NewWay (27+35)
{a=[apple, avocado], b=[banana, blueberry], c=[cherry]}
a/b/c.txt on 2026-09-15
Output: 01-old-vs-new.txt, which holds all four runs (both programs, on both JDKs). The mental model is short. A module is a named group of packages, and the JDK itself is split into 69 of them in the Temurin 25 build I used (java --list-modules). java.base is the one that holds everything you meet on day one: java.lang, java.util, java.io, java.nio.file, java.time, java.util.stream and more. import module java.base; is like writing an on-demand import (import java.util.*;) for every package that module exports. The specification says so directly: it imports, on demand, all public top-level classes and interfaces of the exported packages.
java --describe-module java.base on JDK 25 (it is in output/04); that count is a property of the JDK build, not a promise, and it will change between releases. The last line under the diagram is the part worth remembering: import lines only tell the compiler how to resolve simple names. They leave no trace in the class file, which the next block shows directly:
$ cmp OldWay.class NewWay.class (javac -g:none; NewWay renamed to OldWay inside the class file)
identical, byte for byte (2249 bytes)
Output: 07-bytecode-identical.txt. With debug information off (-g:none), the two class files are identical byte for byte once the class name inside is renamed. With the default settings, the only differences javap -c -p -l finds are the line numbers in the debug table, because the new file has six fewer lines above main. So there is no runtime cost, no startup cost and no change in what the JVM loads.
It is a compile-time convenience, nothing more. The module import does not add the module to your program’s dependencies, does not put anything on a path, and does not make a class loadable that was not loadable before. It only widens the set of simple names the compiler will accept. Everything about readability (which modules your code may use at all) still comes from where you run it, which is what the rest of this article is about.
Going deeper: the small mistakes, and the --release trap
A module that does not exist is an error naming the module, and the statement only belongs at the top of the file, next to the other imports; put it inside a class and the parser rejects it:
$ javac broken/NoSuchModule.java (25.0.4.1+1-LTS)
broken/NoSuchModule.java:1: error: imported module not found: java.nosuch
import module java.nosuch;
^
1 error
$ javac broken/ModuleInsideClass.java (25.0.4.1+1-LTS)
broken/ModuleInsideClass.java:2: error: illegal start of type
import module java.base;
^
broken/ModuleInsideClass.java:2: error: ';' expected
import module java.base;
^
broken/ModuleInsideClass.java:2: error: <identifier> expected
import module java.base;
^
3 errors
Sources: NoSuchModule.java and ModuleInsideClass.java, output in 08-errors-and-release-levels.txt. The syntax needs a Java 25 language level. Compiling the same file with an older --release from a JDK 25 compiler names the fix, and a JDK 21 compiler, which has never heard of the feature, reports a confusing parse error instead:
$ javac --release 24 src/NewWay.java (25.0.4.1+1-LTS)
src/NewWay.java:1: error: module imports are not supported in -source 24
import module java.base;
^
(use -source 25 or higher to enable module imports)
1 error
exit=1
$ javac src/NewWay.java (21.0.10+7-Ubuntu-124.04)
src/NewWay.java:1: error: '.' expected
import module java.base;
^
1 error
The same file shows --release 21 failing identically and --release 25 compiling with exit=0. A library that still has to build with --release 21 or 24 for compatibility cannot use import module in its sources; that limits the feature to code compiled at 25 or later. The JLS also says that naming the same module twice is harmless (the second is redundant); I checked that in DuplicateImports.java, output in 03-precedence.txt.
Going deeper on this section
- Companion repo: jep511 README (how to regenerate every transcript)
- Official reference: JLS 7.5.5, Single-Module-Import Declarations
- Official reference: JEP 511: Module Import Declarations
- Related on this site: Compact Source Files and Instance Main Methods in Java 25 (JEP 512) — the other Java 25 change that removes ceremony from the first lines of a file
When two modules export the same name: the ambiguity error
The price of importing a whole module is that you now import every name in it, including the ones you never wanted. The classic case:Date. java.base has java.util.Date and java.sql has java.sql.Date. Import both modules and ask for Date, and the compiler has no way to choose:
import module java.base;
import module java.sql;
public class AmbiguousDate {
public static void main(String[] args) {
Date d = new Date(0L);
System.out.println(d);
}
}
Source: AmbiguousDate.java.
$ javac broken/AmbiguousDate.java (25.0.4.1+1-LTS)
broken/AmbiguousDate.java:6: error: reference to Date is ambiguous
Date d = new Date(0L);
^
both class java.sql.Date in java.sql and class java.util.Date in java.util match
broken/AmbiguousDate.java:6: error: reference to Date is ambiguous
Date d = new Date(0L);
^
both class java.sql.Date in java.sql and class java.util.Date in java.util match
2 errors
Output: 02-ambiguity.txt (the JDK 27 run is identical). The message names both candidates and the module each one comes from, which makes it easy to fix. It is reported twice because Date is written twice on that line: once as the variable’s type and once in new Date(...). A less obvious version of the same clash comes from java.desktop, which contains java.awt.List:
$ javac broken/AmbiguousList.java (25.0.4.1+1-LTS)
broken/AmbiguousList.java:6: error: reference to List is ambiguous
List<String> names = new ArrayList<>();
^
both class java.awt.List in java.awt and interface java.util.List in java.util match
1 error
Source: AmbiguousList.java. If you import java.desktop for a Swing or AWT class and also use List, this is what you get. The fix is to add one ordinary import for the name you meant:
import module java.base;
import module java.sql;
import java.util.Date; // a single-type import wins over both module imports
public class FixedDate {
public static void main(String[] args) {
Date d = new Date(0L);
System.out.println(d.getClass().getName());
java.sql.Date s = new java.sql.Date(0L); // the other one is still reachable by its full name
System.out.println(s.getClass().getName());
}
}
Source: FixedDate.java.
$ javac src/FixedDate.java && java FixedDate (25.0.4.1+1-LTS)
java.util.Date
java.sql.Date
Output: 02-ambiguity.txt. The single-type import java.util.Date wins over both module imports, and the other Date is still reachable by its full name. So the rule of thumb is: import the module, then pin down any name that clashes with one specific import.
That leaves the question of who beats whom when the old and new styles are mixed. I tested each combination and recorded which Date the program ended up with:
| Imports in the file | What Date means | Source |
|---|---|---|
import java.util.Date; and both module imports | java.util.Date | FixedDate.java |
import java.util.*; and import module java.sql; | java.util.Date | StarImportWins.java |
import java.sql.*; and import module java.base; | java.sql.Date | StarImportWinsSql.java |
a class named Date in the same file and import module java.base; | the class in the file | SamePackageWins.java |
import module java.base; and import module java.sql; | ambiguous, does not compile | AmbiguousDate.java |
import java.util.*; and import java.sql.*; | ambiguous, does not compile | TwoStarImports.java |
import java.util.*;) beats a module import in both directions, and a class declared in the file (so, in the same package) beats it too. In other words, of the ways I tested, a module import is the weakest way to bring a name into scope; anything you write more specifically overrides it. Two module imports (or two on-demand imports) that offer the same name are equally weak, so they clash, and the last row shows that is not new behaviour: the old * imports clash the same way.
An ambiguity is only an error where the name is used. UnusedAmbiguity.java importsjava.baseandjava.sqland never mentionsDate; it compiles and runs, on 25 and 27 alike (see 03-precedence.txt). That has a consequence for later: you can add a second module import to a working file and nothing complains until the first line that uses a clashing name.
Date each, and the thick green line is the one specific import. Without the green line, the two blue lines arrive at the same name and the compiler reports an ambiguity; with it, the specific import settles the matter. That is the whole trick, and it is why a project can adopt import module a file at a time without a big-bang change.
Going deeper on this section
- Companion repo: broken/ (the sources that must fail) and run.sh
- Official reference: JLS 6.4.1, Shadowing (my table above is observed behaviour; the JLS gives the general rule)
- Related on this site: JEP 512 post, where a compact source file gets
java.baseimported implicitly and shows the same clashes
What one module import actually brings in (and what it leaves out)
A module import is not just the module’s own packages. The specification also imports the packages of the modules that module requires transitively, because a module’s API often mentions their types. The JDK’s own descriptions show which is which:$ java --describe-module java.sql (25.0.4.1+1-LTS)
[email protected]
exports java.sql
exports javax.sql
requires java.transaction.xa transitive
requires java.base mandated
requires java.xml transitive
requires java.logging transitive
uses java.sql.Driver
Output: 04-what-a-module-import-brings-in.txt. java.sql exports two packages and lists three requires ... transitive lines: java.xml, java.logging, java.transaction.xa. The odd one out, requires java.base mandated, is different: every module gets that one automatically, and it is not marked transitive. So importing java.sql gives you the SQL classes plus the XML, logging and XA packages, and does not give you java.util or java.lang. This program uses one class from each to show it:
import module java.base;
import module java.sql;
public class Transitive {
public static void main(String[] args) {
// java.sql declares "requires transitive" java.logging, java.xml and java.transaction.xa,
// so importing java.sql also imports their exported packages.
Logger log = Logger.getLogger("demo"); // java.util.logging (java.logging)
System.out.println(log.getName());
System.out.println(XAResource.TMNOFLAGS); // javax.transaction.xa (java.transaction.xa)
System.out.println(DocumentBuilderFactory.class.getModule().getName()); // javax.xml.parsers (java.xml)
List<String> l = List.of("java.base needed its own import");
System.out.println(l);
}
}
Source: Transitive.java.
$ javac src/Transitive.java && java Transitive (25.0.4.1+1-LTS)
demo
0
java.xml
[java.base needed its own import]
Output: 04-what-a-module-import-brings-in.txt. Logger, XAResource and DocumentBuilderFactory all resolved without an import of their own module, and the run confirms they came from java.logging, java.transaction.xa and java.xml. The last line uses List, and it only works because the file also says import module java.base;. Remove that line and you get this:
$ javac broken/SqlDoesNotGiveBase.java (25.0.4.1+1-LTS)
broken/SqlDoesNotGiveBase.java:5: error: cannot find symbol
List<String> l = List.of("x"); // java.util.List lives in java.base
^
symbol: class List
location: class SqlDoesNotGiveBase
broken/SqlDoesNotGiveBase.java:5: error: cannot find symbol
List<String> l = List.of("x"); // java.util.List lives in java.base
^
symbol: variable List
location: class SqlDoesNotGiveBase
2 errors
Source: SqlDoesNotGiveBase.java. The trap is that it looks like a bug in your code: List is right there in the standard library. It fails the same way on JDK 27. If you use a module import for anything other than java.base, keep import module java.base; beside it.
java.sql, and the red dashed one is java.base, which does not. The diagram shows the readability rule the JLS states (section 7.5.5): an import brings in the modules the imported one requires transitively, and only those.
The other surprise is java.se. It is the JDK’s umbrella module for the whole Java SE API, and the JLS uses it as its own example of a module worth importing even though it exports nothing. Here is what the JDK says about it, and what happens when you import it:
$ java --describe-module java.se | grep -c exports (25.0.4.1+1-LTS)
0
$ javac src/JavaSe.java (25.0.4.1+1-LTS)
src/JavaSe.java:1: error: unnamed module does not read: java.se
import module java.se;
^
1 error
$ javac --add-modules java.se src/JavaSe.java && java JavaSe (25.0.4.1+1-LTS)
[a] x 0
Output: 04-what-a-module-import-brings-in.txt, source JavaSe.java. java.se has zero exports lines (and twenty requires ... transitive lines, counted in the same file). javac refuses the import with unnamed module does not read: java.se, on 25 and 27. Adding --add-modules java.se to the compile makes it work. My reading is that a plain classpath compile only resolves the modules that export something by default and java.se exports nothing, so it is not read until you ask; I confirmed the symptom and the fix, not the reason inside javac. Note also the import java.util.List; in that file: java.se brings in java.desktop and with it java.awt.List, so List would be ambiguous without it.
import module java.se;is a very wide net. It pulls in twenty modules, includingjava.desktop,java.sqlandjava.xml. Every well-known simple name that appears in more than one of them becomes ambiguous the moment you use it. It is fine for a throwaway script, and a poor habit for anything a colleague will read.
Going deeper on this section
- Companion repo: output/04 has the full
--describe-moduleoutput and both JDKs’ runs - Official reference: java.se module summary (the API documentation for the umbrella module)
- Official reference: JLS 7.3, Compilation Units (which modules a compilation unit reads)
Packages, module-info.java and libraries
Two things people ask early: does it matter that my class is in a package, and does it work in a project that has a module-info.java? The first is easy. A file in a named package uses it exactly like one in the default package:
package demo.mod;
import module java.base;
public class InPackage {
public static void main(String[] args) {
System.out.println(List.of(1, 2, 3) + " from package " + InPackage.class.getPackageName());
}
}
Source: InPackage.java.
$ javac src/InPackage.java && java demo.mod.InPackage (25.0.4.1+1-LTS)
[1, 2, 3] from package demo.mod
Output: 05-packages-and-modules.txt. The second is where readability starts to matter. If your own code is a named module, a module import is only legal for modules that module reads, which means the ones it lists in requires. I built two tiny apps to show both sides. This one imports java.sql and never requires it:
module demo.noreq { }
package demo;
import module java.sql; // demo.noreq never says "requires java.sql"
public class NoReq {
public static void main(String[] args) {
System.out.println(Connection.TRANSACTION_NONE);
}
}
Sources: module-info.java and NoReq.java.
$ javac -d out --module-source-path multi --module demo.noreq (25.0.4.1+1-LTS)
multi/demo.noreq/demo/NoReq.java:3: error: module demo.noreq does not read: java.sql
import module java.sql; // demo.noreq never says "requires java.sql"
^
1 error
Output: 05-packages-and-modules.txt. The message is exact about what is missing: module demo.noreq does not read: java.sql. Adding requires java.sql; to module-info.java fixes it, which is the same edit you would have made before the feature existed. The import line adds no dependency of its own; it only borrows one you already declared.
That also makes it useful for third-party modules. acme.text below is a small library I wrote for this article with one exported package and one hidden one:
module acme.text {
exports acme.text; // acme.text.internal is NOT exported
}
package demo;
import module acme.text; // imports Slug, but not acme.text.internal.Helper
public class App {
public static void main(String[] args) {
System.out.println(Slug.of("Import Module: Hello, World!"));
}
}
Sources: acme.text/module-info.java, Slug.java, Helper.java, demo.app/module-info.java and App.java.
$ javac -d out --module-source-path multi --module acme.text,demo.app && java -p out -m demo.app/demo.App (25.0.4.1+1-LTS)
import-module-hello-world
Output: 05-packages-and-modules.txt. import module acme.text; got me Slug and nothing more. To check that the hidden package is not imported, Bad.java tries to use Helper from acme.text.internal, and gets cannot find symbol on both JDKs. That is the point of the mechanism: only what a module exports is imported, so the import cannot become a way around a module’s encapsulation.
$ javac -p lib src/ThirdParty.java (25.0.4.1+1-LTS)
src/ThirdParty.java:1: error: unnamed module does not read: acme.text
import module acme.text;
^
1 error
$ javac -p lib --add-modules acme.text src/ThirdParty.java && java -p lib --add-modules acme.text -cp out ThirdParty (25.0.4.1+1-LTS)
a-script-using-a-library-module
Source: ThirdParty.java, output in 05-packages-and-modules.txt. Putting the library on the module path is not enough; the unnamed module does not read it until --add-modules acme.text says so. This is the same rule as java.se in the previous section.
Going deeper on this section
- Companion repo: multi/ (the three modules and the deliberate failures)
- Official reference:
javaccommand reference (--module-source-path,--add-modules) - Related on this site: Flexible Constructor Bodies in Java 25 (JEP 513), the same series, which uses the same
run.sh+ transcript layout
Scripts, compact source files and jshell: where it earns its keep
The natural home forimport module is code where nobody is going to review a long import list: a script, a teaching example, a quick experiment. That is also where Java 25’s other beginner-oriented feature lives. In a JEP 512 compact source file, java.base is already imported for you, so a program with no imports at all can still say List. A normal class cannot:
void main() {
IO.println(List.of("no imports at all"));
}
public class ClassicNoImport {
public static void main(String[] args) {
System.out.println(List.of("no imports at all"));
}
}
Sources: CompactNoImport.java and ClassicNoImport.java.
$ javac src/CompactNoImport.java && java CompactNoImport (25.0.4.1+1-LTS)
[no imports at all]
$ javac broken/ClassicNoImport.java (25.0.4.1+1-LTS)
broken/ClassicNoImport.java:3: error: cannot find symbol
System.out.println(List.of("no imports at all"));
^
symbol: variable List
location: class ClassicNoImport
1 error
Output: 06-compact-files-and-scripts.txt. So in a compact file you only add module imports for other modules, and the implicit java.base import behaves like an explicit one when names clash:
import module java.sql;
void main() {
var l = List.of("java.base came implicitly"); // no import module java.base written
IO.println(l + " " + Connection.TRANSACTION_NONE); // java.sql from the explicit import
}
$ javac src/CompactPlusImport.java && java CompactPlusImport (25.0.4.1+1-LTS)
[java.base came implicitly] 0
Source: CompactPlusImport.java, output in 06-compact-files-and-scripts.txt. List comes from the implicit import, Connection from the explicit java.sql one, and there is no clash because neither name exists in both. Try Date in the same file (CompactAmbiguous.java) and you get the same reference to Date is ambiguous error as before, with java.util.Date from the implicit import on one side and java.sql.Date on the other. It works with the source launcher too, so a one-file script needs no build step:
import module java.base;
void main() {
var now = LocalDate.of(2026, 9, 15);
IO.println(Stream.of("a", "b", "c").collect(Collectors.joining("-")) + " " + now);
}
$ java src/Script.java (25.0.4.1+1-LTS)
a-b-c 2026-09-15
Source: Script.java, output in 06-compact-files-and-scripts.txt (same result on 27).
jshell, because it has always pre-imported a fixed list of packages. Here is what each JDK says its default imports are:
$ jshell --feedback concise src/imports.jsh (21.0.10+7-Ubuntu-124.04)
import java.io.*
import java.math.*
import java.net.*
import java.nio.file.*
import java.util.*
import java.util.concurrent.*
import java.util.function.*
import java.util.prefs.*
import java.util.regex.*
import java.util.stream.*
[2, 4]
Error:
cannot find symbol
symbol: variable LocalDate
System.out.println(LocalDate.of(2026, 9, 15))
^-------^
$ jshell --feedback concise src/imports.jsh (25.0.4.1+1-LTS)
import java.base
[2, 4]
2026-09-15
Source: imports.jsh, output in 09-jshell.txt. JDK 21 lists ten * imports and JDK 25 lists a single java.base entry; 27 shows the same as 25. The [2, 4] line is the same on all three, so the behaviour you relied on (collections, streams) did not change. What changes is visible in the last line of the script: LocalDate, which was not in the old list, fails with cannot find symbol on JDK 21 and prints 2026-09-15 on 25 and 27, with no import typed. Anything in java.base, not just the ten packages in the old list, is now there for you.
Scripts and teaching: yes. A large codebase: decide as a team. The feature costs nothing at runtime and removes a real barrier for a beginner. In a long-lived application it trades an explicit list (which shows a reviewer exactly what the file depends on) for a shorter one. The two downsides I can back up from these tests are the ambiguity errors that appear as soon as two module imports overlap, and that the file stops compiling below Java 25. What I did not test is how IDEs, formatters, import-organising plugins, Maven and Gradle treat the syntax; check those on your toolchain before you adopt it across a team.
Going deeper: how I checked that nothing about the class file changes
Two class files are built from OldWay.java and NewWay.java, and the script in run.sh compares them three ways: the disassembly with the class name normalised, the raw bytes with debug info removed, and the disassembly with line numbers included.
$ diff <(javap -c -p -l OldWay) <(javap -c -p -l NewWay) (default javac -g keeps line numbers)
8c8
< line 9: 0
---
> line 3: 0
52,57c52,57
< line 11: 0
< line 12: 14
< line 13: 25
< line 14: 37
< line 15: 44
< line 16: 90
---
> line 5: 0
> line 6: 14
> line 7: 25
> line 8: 37
> line 9: 44
> line 10: 90
67c67
< line 13: 0
---
> line 7: 0
Output: 07-bytecode-identical.txt. Every difference is a line N: offset entry, and every line number is lower in the new file by six, which is the number of import lines it does not have. That is the expected footprint of removing lines above the code, and it is also why the same stack trace would point at a different line number after you convert a file.
Going deeper on this section
- Companion repo: src/ (every runnable example in this article)
- Official reference:
jshellcommand reference - Related on this site: Compact Source Files and Instance Main Methods in Java 25 (JEP 512)
Should you use this?
Yes, in three places. In a compact source file or a script, where the alternative is a page of imports that adds nothing to a reader’s understanding. Injshell, where you get it without doing anything. And when you are teaching, because a first-day student who has to learn seven import lines before writing their first loop is learning the wrong thing first.
Be more careful in application code. Stick with the specific imports (or the * ones you already have) where the file is read by people who want to see its dependencies at a glance, and where you may need to compile below Java 25. If you do adopt it, keep three habits: pair every non-java.base module import with an explicit import module java.base;, resolve a clash with one specific import rather than by dropping the module import, and avoid java.se outside throwaway code.
What I did not test. I did not test the earlier preview versions of this feature (which is why I say nothing about JDK 23 or 24 behaviour), any tool outside the JDK (IDEs, Maven, Gradle, formatters), or what happens when a future JDK adds a class to an existing module that clashes with a name you already use. That last case follows from how on-demand imports work, but I have not seen it happen, so treat it as a reasoned risk and not an observation.
Further reading
- Companion repository for this article: javademos, jep511 module
- Official reference: JEP 511: Module Import Declarations
- Official reference: JLS 7.5.5, Single-Module-Import Declarations (Java SE 25)
- Related on this site: Compact Source Files and Instance Main Methods in Java 25 (JEP 512)
- Related on this site: Flexible Constructor Bodies in Java 25 (JEP 513): Validate Before super()
- Related on this site: Java 27 Is Out: Every JEP, Plus the Java 26 Changes You Skipped
No Comments yet!