From Packages to Modules: How JDK 25 Reworks Java Imports
For many Java developers, import statements are a small but persistent source of friction. Beginners may wonder why String is available without an explicit import while List is not. Experienced developers can still lose time locating Stream, resolving the package for Date, or dealing with classes that share the same name.
JDK 25's JEP 511, Module Import Declarations, changes that experience by allowing imports at the module level. Instead of listing individual classes or packages, a developer can import an entire module and gain access to all of its exported packages. The result is a shorter, more modular way to express dependencies—sometimes reducing dozens of import declarations to a single line.
Why Java's traditional imports have been frustrating
Java has historically provided two main import styles, and neither perfectly balances brevity, transparency, and maintainability.
Explicit class imports
An explicit import names one class directly:
import java.util.List;
This makes dependencies easy to see. IDEs can provide accurate completion and refactoring support, and large codebases benefit from knowing exactly which types a class uses.
The trade-off is verbosity. A class that combines file I/O, collections, streams, mathematical utilities, and other APIs can accumulate a long list of imports. The resulting header may occupy many lines, while a missing or incorrect package path leads to compilation errors.
Wildcard imports
A wildcard import reduces that list:
import java.util.*;
It includes the classes directly within the specified package, but it does not recursively include subpackages. For example, java.util.* does not make types in java.util.stream or java.util.function available.
Wildcard imports also make dependencies less obvious. A reader cannot immediately tell which types from the package are actually used, and classes with the same simple name can create ambiguity. java.util.Date and java.sql.Date are a familiar example: importing both packages with wildcards leaves the compiler unable to determine which Date is intended.
Why String needs no import
The reason String, Integer, and other familiar types can be used without an explicit import is Java's implicit import of java.lang.*. The java.lang package contains fundamental language classes, wrapper types, exception types, and other components used throughout ordinary Java programs.
This convenience can also confuse newcomers. It is easy to assume that all commonly used classes are implicitly available, when in reality only the classes in java.lang receive this treatment.
Wildcards do not slow down the running program
Another long-standing misconception is that wildcard imports reduce runtime performance. Imports are a compile-time mechanism. They help the compiler resolve names; they do not determine how classes are loaded or executed at runtime.
After compilation, references are represented using fully qualified class names such as java.util.List, regardless of whether the source used an explicit import or a wildcard. The resulting bytecode structure, class-loading behavior, and runtime performance are therefore not affected by the choice between these two import styles. The real concern with wildcards is readability and maintenance, not execution speed.
What module-level imports add in JDK 25
Module-level imports build on the Java Platform Module System introduced in JDK 9. The key change is that the unit of import is no longer limited to a package or an individual class. A module can expose a coordinated set of packages, and a module import makes those exported packages available together.
Modules and exported packages
Since JDK 9, the Java class library has been divided into modules such as:
java.base, which contains core language and foundational APIs;java.sql, which provides database-related APIs; andjava.xml, which contains XML-processing functionality.
A module describes its public surface in module-info.java. The exports directive identifies packages that other modules may access. Packages that are not exported remain encapsulated, protecting implementation details from external dependencies.
The java.base module illustrates why module-level imports can be useful. Its module descriptor declares 54 exported packages, including java.lang, java.util, java.io, java.math, and java.util.stream. Together, these packages cover collections, input and output, mathematical operations, stream processing, and many other common development tasks.
A module-level import therefore does not mean that every package in the module becomes visible indiscriminately. It grants access to the module's exported packages, while non-exported internal packages remain inaccessible.
The syntax
The syntax is deliberately compact:
import module 模块名称;
The module name is its unique identifier, such as java.base or java.sql. For the core platform module, the declaration is:
import module java.base;
This makes types from the exported packages of java.base available, including java.lang.String, java.util.List, java.io.BufferedReader, java.util.stream.Stream, and java.math.BigDecimal, without separate package-level imports.
A practical comparison
Consider a small program that reads a file and sorts its lines. Before module-level imports, the source might look like this:
// 传统导入需要逐个引入用到的包和类
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class FileSortDemo {
public static void main(String[] args) throws IOException {
List<String> lines = new ArrayList<>();
// 读取文件内容
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
String line;
while ((line = br.readLine()) != null) {
lines.add(line);
}
}
// 使用Stream API排序
List<String> sortedLines = lines.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedLines);
}
}
With JDK 25, the import section can be reduced to one module declaration:
// 一行导入java.base模块,涵盖所有核心包
import module java.base;
public class FileSortDemo {
public static void main(String[] args) throws IOException {
List<String> lines = new ArrayList<>();
try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
String line;
while ((line = br.readLine()) != null) {
lines.add(line);
}
}
List<String> sortedLines = lines.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedLines);
}
}
The business logic has not changed. What disappears is the need to remember that List belongs to java.util, BufferedReader to java.io, and stream-related types to java.util.stream.
Combining modules for more specialized code
java.base handles a large share of everyday Java development, but applications that work with databases, XML, or other specialized APIs may need additional modules. Module-level imports can be combined:
// 核心基础模块 + 数据库模块组合导入
import module java.base;
import module java.sql;
public class JdbcDemo {
public static void main(String[] args) throws Exception {
// 加载MySQL驱动(需引入MySQL驱动依赖)
Class.forName("com.mysql.cj.jdbc.Driver");
// 直接使用java.sql模块下的类,无需单独导入
try (Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test", "root", "password")) {
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM user");
while (rs.next()) {
System.out.println(rs.getString("username"));
}
}
}
}
Here, java.base supplies the foundational APIs and java.sql supplies types such as Connection, Statement, and ResultSet. The database driver itself still has to be provided as a project dependency; importing a module does not install third-party software.
Handling name collisions
The convenience of importing several modules can expose a familiar problem: two exported packages may contain classes with the same simple name. For example, both java.base and java.sql make a Date type available through different packages.
The effective precedence is:
explicit class import > module-level import > wildcard import
An explicit import can therefore settle an ambiguity created by module imports:
// 导入两个存在同名类的模块
import module java.base;
import module java.sql;
// 精确导入指定使用java.util.Date,解决冲突
import java.util.Date;
public class DateDemo {
public static void main(String[] args) {
// 直接使用java.util.Date(由精确导入指定)
Date utilDate = new Date();
System.out.println("java.util.Date:" + utilDate);
// 如需使用java.sql.Date,通过全限定名指定
java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());
System.out.println("java.sql.Date:" + sqlDate);
}
}
The same pattern works beyond Date: use a precise import when one type should be the default, and use a fully qualified name when both colliding types are needed in the same source file.
Importing custom modules
Module-level imports are not limited to modules shipped with the JDK. A project can define its own module, export the packages intended for external use, and then import the module as a unit.
For example, a custom module named com.example.utils could declare:
// 自定义模块的模块描述文件
module com.example.utils {
// 导出对外暴露的工具包
exports com.example.utils.string; // 字符串工具包
exports com.example.utils.collection; // 集合工具包
}
Suppose com.example.utils.string contains StringUtils, while com.example.utils.collection contains CollectionUtils. Another module can use them through a module import:
// 导入自定义模块
import module com.example.utils;
public class CustomModuleDemo {
public static void main(String[] args) {
// 直接使用自定义模块导出包中的类,无需关注具体包路径
String trimmedStr = StringUtils.trim(" hello module import ");
boolean isEmpty = CollectionUtils.isEmpty(new ArrayList<>());
System.out.println("修剪后的字符串:" + trimmedStr);
System.out.println("集合是否为空:" + isEmpty);
}
}
This is especially useful in larger applications that divide functionality among several internal modules. Consumers can depend on the module's public surface without having to memorize every utility package.
Important boundaries and cautions
Only exported packages are available
A module import does not bypass encapsulation. If a package is not listed in the module's exports declarations, its types remain unavailable to code outside the module.
For example, the following attempt to use sun.misc.Unsafe fails because sun.misc is not exported by java.base:
import module java.base;
public class UnsafeDemo {
public static void main(String[] args) {
// 编译报错:sun.misc包未被java.base模块导出,无法访问
sun.misc.Unsafe unsafe = sun.misc.Unsafe.getUnsafe();
}
}
This restriction is an intentional part of the module system. It protects implementation packages from external coupling and reduces the risk that internal changes will break dependent applications.
Choose the import style according to the project
Module-level imports are particularly attractive for rapid development, prototypes, small utilities, educational projects, and code where reducing setup overhead matters. They let developers focus on APIs and behavior instead of package lookup.
Large frameworks, open-source libraries, and enterprise systems with strict dependency policies may benefit more from explicit imports. In those environments, a visible list of type-level dependencies can make reviews, refactoring, version upgrades, and long-term maintenance easier.
A practical rule is to use module imports for efficiency in ordinary development, while retaining explicit imports where dependency transparency is a priority. Module-level imports complement the existing mechanisms; they do not make explicit imports obsolete.
JDK and IDE requirements
Using the feature requires a JDK version that supports it—JDK 25 or later. The development environment must also recognize module import syntax. The source specifically identifies IntelliJ IDEA 2025.1+ and Eclipse 2025-06+ as versions with full support, provided the project is configured to use JDK 25 and module-system support is enabled.
Older IDEs may show syntax highlighting or inspection errors even when the code can be compiled and run by the correct JDK. Upgrading the IDE, or temporarily disregarding an outdated inspection, may be necessary.
A broader shift in Java's model of dependencies
Java's import history—from the implicit availability of java.lang, through wildcard imports, to module-level imports—reflects a gradual effort to improve developer experience without abandoning the language's emphasis on structure and compatibility.
Traditional imports encourage developers to think in terms of packages and individual classes. Module-level imports shift some of that attention toward functional units: core APIs, database functionality, XML support, or an application's own utility module. This is closer to the way modern developers often reason about dependencies—as groups of capabilities rather than as isolated package paths.
The change has different benefits for different audiences. Beginners spend less time memorizing package locations. Experienced developers avoid repetitive import maintenance and package searches. Teams can also establish clearer conventions, such as using module imports for foundational JDK APIs while keeping third-party and internal types explicit.
More importantly, the feature gives the Java 9 module system a more visible role in daily coding. JDK 9 introduced modules to improve dependency management and encapsulation, but source files continued to use the older package-oriented import model. Module-level imports connect those two parts of the platform and make the module boundary more practical at the point where developers declare dependencies.
Practical adoption guidelines
A sensible adoption path is straightforward:
- Move the development environment to JDK 25 or later and verify IDE support.
- Use
import module java.base;when a source file depends broadly on the core platform APIs. - Add module imports for specialized functionality such as
java.sqlwhen needed. - Resolve collisions with an explicit import or a fully qualified class name.
- Keep explicit imports as the default in projects where dependency visibility and strict control matter.
- Establish a team convention so that different developers do not apply conflicting import styles without a clear reason.
Module-level imports offer a concise way to express broad dependencies, but their value depends on using them deliberately. In a small tool or prototype, one line can remove a great deal of ceremony. In a carefully layered framework or library, the same convenience may conceal information that maintainers need to see. The most useful approach is therefore not to replace every existing import, but to choose the level of detail that fits the codebase.