
In this article, you will learn:
- What does an import do, and which kinds of imports does Java have?
- How do you import entire Java modules using
import module? - How do you resolve ambiguities in module imports?
- What are transitive module imports, and how do they work?
- Which modules are imported by default?
Let’s start with the classic imports that have existed since Java 1.0.
Java Imports
What an Import Does
You refer to a class from another package by its fully qualified name, i.e., with the package name in front of it:
public class Names {
java.util.List<String> names = new java.util.ArrayList<>();
}
An import makes a class available by its simple name. With two imports, the example becomes:
import java.util.ArrayList;
import java.util.List;
public class Names {
List<String> names = new ArrayList<>();
}
An import does not load a class and does not generate any bytecode. It only tells the compiler which classes List and ArrayList refer to. If you compile both variants with javac -g:none (without the debug information that contains the line numbers), the .class files are byte-for-byte identical – both contain the fully qualified names java/util/List and java/util/ArrayList.
And String? It needs no import because Java imports the java.lang package automatically – more on that under Imported Automatically: java.lang.
Importing Classes and Packages
Since Java 1.0, we have been able to import individual classes (“single-type-import declaration”) or entire packages (“type-import-on-demand declaration”) using the import statement, e.g.:
import java.util.*;
import java.util.stream.Stream;
A package import covers only the classes of exactly that package, not those of its subpackages. So import java.util.*; imports java.util.List, but not java.util.stream.Stream – that is what the second import in the example is for. Without it, javac aborts with “cannot find symbol” as soon as you use Stream.
Static Imports
Since Java 5, you can also use import static to import static fields and methods of a class – individually (“single-static-import declaration”) or all at once (“static-import-on-demand declaration”):
import static java.lang.Math.PI; // ← only the constant PI
import static java.lang.Math.*; // ← all static members of Math
public class Circle {
double area = PI * pow(2, 2);
}
Without the static imports, you would have to write Math.PI and Math.pow(2, 2).
Imported Automatically: java.lang
The classes of the java.lang package have always been imported automatically. Therefore, we do not need to specify import statements for classes like Object, String, Integer, Exception, etc.
The Java Language Specification describes this as if every source file contained import java.lang.*; directly after the package declaration. It follows that subpackages such as java.lang.reflect are not imported automatically.
Where Imports Go
A Java source file has a fixed order: first the optional package declaration, then the imports, then the classes. If an import comes after the class, the compiler aborts:
public class Order {}
import java.util.List;
Order.java:3: error: class, interface, annotation type, enum, record, method or field expected
import java.util.List;
^
1 error
The same message appears if the package declaration comes after an import.
Are There Import Aliases (“import … as”) in Java?
No. Unlike Python or Kotlin, for example, Java has no way to import a class under a different name. javac rejects import java.util.List as JList; with the message ';' expected.
If a class needs two classes with the same simple name, you import one of them and write the other one fully qualified:
import java.util.List;
public class TwoLists {
List<String> names; // ← java.util.List
java.awt.List awtList; // ← fully qualified
}
Since Java 25, the Java Language Specification has five kinds of import declarations: the four from this chapter and the “single-module-import declaration”, which the rest of this article is about.
Module Import Declarations
Starting with Java 25 (and in preview mode in Java 23 and Java 24), we can use import module to import entire modules. This allows us to directly use all classes that are part of a module and exported by it.
The general form is:
import module <module name>;
In the following example, we import the module java.base and can, therefore, use the classes List, Map, Stream, and Collectors without having to import them individually or by package (String and Character are in the package java.lang and have always been imported automatically).
import module java.base;
public class ModuleImportTest {
public static Map<Character, List<String>> groupByFirstLetter(String... values) {
return Stream.of(values).collect(
Collectors.groupingBy(s -> Character.toUpperCase(s.charAt(0))));
}
}
If you save this code example in the file ModuleImportTest.java, you can compile the class as follows since Java 25 (in Java 23/24, import module was a preview feature and had to be enabled with --enable-preview --source <version>):
javac ModuleImportTest.java
To use import module, it is not necessary for the importing class itself to be in a module, as seen in the previous example.
Named Modules Only: Class Path and Module Path
The imported module must be a named module, and it must be part of the so-called module graph. That is the set of modules the compiler knows when it compiles your code. It builds the graph from a few root modules and everything they need via requires. For code on the class path, almost all JDK modules are roots – so they are always in the graph.
A library on the class path, on the other hand, belongs to the unnamed module and cannot be imported with import module. javac then reports “imported module not found”.
If the library is on the module path as a module, that alone does not put it into the module graph. The compiler only adds a module from the module path if a module in the graph names it in a requires directive. If it is missing, javac reports “unnamed module does not read”, followed by the module name. For code on the class path, you make such a module a root yourself with --add-modules:
javac --module-path <path to JAR> --add-modules <module name> App.java
If your own code is a module, a requires <module name>; in its module-info.java is enough instead.
Automatic java.base Import in Compact Source Files
Compact Source Files (previously “Unnamed Classes”, “Implicitly Declared Classes”, and “Simple Source Files”) automatically import the java.base module, i.e., you can use classes like List and Map there without explicit imports.
Ambiguous Class Names
Sometimes, a class name is not unique. In the following example, there is a List type in both the imported module java.base (java.util.List) and the module java.desktop (java.awt.List):
import module java.base; // ← Contains java.util.List
import module java.desktop; // ← Contains java.awt.List
public class Ambiguous {
List list; // ← Ambiguous reference to List
}
If you save the file as Ambiguous.java and then compile it as follows:
javac Ambiguous.java
... then the compiler will abort with the following error message:
Ambiguous.java:5: error: reference to List is ambiguous
List list; // ← Ambiguous reference to List
^
both class java.awt.List in java.awt and interface java.util.List in java.util match
1 error
This means that the compiler does not know which of the two List types you mean.
Resolving Ambiguous Class Names
Let’s assume you want to use java.util.List (and not java.awt.List). Then you have two options to resolve this ambiguity:
Option 1: You also import the class java.util.List directly:
import module java.base;
import module java.desktop;
import java.util.List; // ← Ambiguity resolved by single-type-import declaration
public class Ambiguous {
List list;
}
Option 2: You also import the package java.util (in Java terminology, this is not called “package import” but “type-import-on-demand declaration”):
import module java.base;
import module java.desktop;
import java.util.*; // ← Ambiguity resolved by type-import-on-demand declaration
public class Ambiguous {
List list;
}
The second option has only been available since Java 24. Compiling the last example with Java 23 also results in the error message “reference to List is ambiguous.”
Both options follow the same rule: the more specific import takes precedence over the more general one.
| Kind of import | Example | Takes precedence over |
|---|---|---|
| Single-type import | import java.util.List; | package and module imports |
| Type-import-on-demand | import java.util.*; | module imports (since Java 24) |
| Module import | import module java.base; | no other import |
Transitive Module Imports
When a module imported with import module requires another module with requires transitive, you can also use all classes of the exported packages of that other module without explicit imports.
I want to explain this using an example with the modules java.sql and java.xml.
- The
java.sqlmodule has, among others, a transitive dependency on the modulejava.xml. - The
java.sqlmodule exports the packagesjava.sqlandjavax.sql. - The
java.xmlmodule exports the packagesjavax.xml,javax.xml.parsers, andorg.w3c.dom, among others.
The following graphic shows the modules, their dependencies, and the exported packages:
The module declaration of the java.sql module looks like this:
module java.sql {
. . .
requires transitive java.xml;
exports java.sql;
exports javax.sql;
. . .
}
And the module declaration of java.xml:
module java.xml {
exports javax.xml;
exports javax.xml.parsers;
. . .
}
If we now write a program that imports the module java.sql, then we do not need explicit imports for the classes SAXParserFactory and SAXParser from the package javax.xml.parsers of the java.xml module – and also no explicit import of this module:
import module java.sql; // ← Also imports the packages of java.xml,
// e.g., javax.xml.parsers
. . .
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();
. . .
That’s because from the transitive dependency of module java.sql on module java.xml and the fact that java.xml exports the package javax.xml.parsers, it follows that the program can also access all classes of the package javax.xml.parsers without explicit imports.
Automatic java.base Import in JShell
Since Java 25, JShell automatically imports the java.base module:
$ jshell
| Welcome to JShell -- Version 27
| For an introduction type: /help intro
jshell> /imports
| import java.base
If you start JShell with Java 24 or older and enter the /imports command, you’ll instead see the following ten default package imports:
$ jshell
| Welcome to JShell -- Version 24
| For an introduction type: /help intro
jshell> /imports
| 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.*
Conclusion
An import makes classes and static members available by their simple names – individually, by package, and since Java 25 also by module. It changes nothing at runtime.
Module Import Declarations can make Java programs shorter and easier to maintain by allowing you to import entire modules rather than individual classes and packages. On the other hand, they can also further fuel the eternal discussion about whether to import classes individually or by package.
In Compact Source Files (previously Unnamed Classes, Implicitly Declared Classes, and Simple Source Files) and JShell, the java.base module is automatically imported so that all classes of this module can be used directly without imports.
I recommend import module wherever you experiment: in JShell, in compact source files, and when prototyping. In large codebases, single-type imports remain the clearer choice: with them, every file shows which package a class comes from – and a new class in an imported module cannot make a previously unique name ambiguous.
Module Import Declarations were a preview feature until Java 24 and had to be enabled with --enable-preview --source <version>. Since Java 25, they are finalized and a permanent part of the language – usable without any additional flags.
What should I cover next? The best way for me to find out is through your feedback: a review on my ProvenExpert profile shows me which topics matter to you – and motivates me to write more articles.




