03、JDK 25 新特性:模块导入声明(JEP 511)简化模块使用

鹏磊我发现,在 Java 9 引入模块系统后,很多库都开始模块化,但传统项目往往还在使用非模块化的代码结构。这导致了一个问题:要使用模块化的库,必须逐个导入包,import 语句堆积如山,代码可读性下降。

JEP 511 的模块导入声明正是为了解决这个痛点。通过 import module 语法,你可以用一个声明导入整个模块的所有导出包,大大简化了模块化库的使用。鹏磊我觉得更重要的是,这个特性让非模块化项目也能轻松使用模块化的第三方库,降低了模块系统的使用门槛。

模块导入声明是啥

模块导入声明(Module Import Declarations)是 JDK 25 引入的一个新语法,允许你通过一个声明导入整个模块导出的所有包。这个特性主要是为了解决模块化代码和非模块化代码之间的互操作问题。

在 Java 9 引入模块系统之后,很多库都开始模块化了,但是很多项目还是用的传统方式,没有 module-info.java 文件。这就导致一个问题:你要用模块化的库,就得知道它导出了哪些包,然后一个个导进来,麻烦得很。

JEP 511 就是为了解决这个问题。它让你可以直接导入整个模块,编译器会自动处理这个模块导出的所有包,你就不用管具体有哪些包了。

基本语法和使用

模块导入声明的语法很简单:

import module <模块名>;

就这么简单,一个声明搞定。比如你要用 java.base 模块里的所有东西,就这么写:

import module java.base;  // 导入 java.base 模块的所有导出包

这样写完之后,java.base 模块导出的所有包你都能用了,包括 java.util、java.util.function、java.util.stream 等等,不用再一个个导了。

咱们看个实际的例子,对比一下传统方式和新的模块导入方式:

// 传统方式:需要一个个包地导入
import java.util.Map;           // 导入 Map 接口
import java.util.List;          // 导入 List 接口
import java.util.function.Function;  // 导入 Function 函数式接口
import java.util.stream.Stream;      // 导入 Stream 流
import java.util.stream.Collectors;  // 导入 Collectors 收集器

public class TraditionalExample {
    public void processData() {
        // 使用导入的类
        Map<String, Integer> map = new HashMap<>();  // 创建映射
        List<String> list = Arrays.asList("a", "b");  // 创建列表
        Function<String, Integer> func = String::length;  // 函数式接口
        Stream<String> stream = list.stream();  // 创建流
        List<Integer> lengths = stream.map(func).collect(Collectors.toList());  // 处理流
    }
}

现在用模块导入声明,就简单多了:

// 新方式:一个声明导入整个模块
import module java.base;  // 一次性导入 java.base 模块的所有导出包

public class ModuleImportExample {
    public void processData() {
        // 直接使用,不用管具体是哪个包
        Map<String, Integer> map = new HashMap<>();  // 直接用,编译器知道在哪
        List<String> list = Arrays.asList("a", "b");  // 列表也能直接用
        Function<String, Integer> func = String::length;  // 函数式接口直接用
        Stream<String> stream = list.stream();  // 流也能直接用
        List<Integer> lengths = stream.map(func).collect(Collectors.toList());  // 收集器也能直接用
    }
}

你看,代码清爽多了,不用写那一堆 import 了。编译器会自动解析 java.base 模块导出的所有包,然后让你用里面的类。

处理导入冲突

当然,这个特性也不是完美的,有个问题就是可能会有冲突。比如两个模块都导出了同名的类,你同时导入这两个模块,编译器就不知道你要用哪个了。

举个例子,java.util 和 java.sql 都有 Date 类:

import module java.base;  // 包含 java.util.Date
import module java.sql;   // 包含 java.sql.Date

public class ConflictExample {
    public void useDate() {
        // 这里就有问题了,编译器不知道你要用哪个 Date
        Date date = new Date();  // 编译错误:Date 类有歧义
    }
}

遇到这种情况,你得显式地指定用哪个类:

import module java.base;  // 导入 java.base 模块
import module java.sql;   // 导入 java.sql 模块

import java.sql.Date;     // 显式导入 java.sql.Date,解决冲突

public class ResolvedConflictExample {
    public void useDate() {
        // 现在编译器知道你要用 java.sql.Date 了
        Date sqlDate = new Date();  // 使用 java.sql.Date
        java.util.Date utilDate = new java.util.Date();  // 或者用全限定名
    }
}

这样就能解决冲突了。编译器会优先使用你显式导入的那个类,如果还有冲突,就用全限定名。

实际应用场景

这个特性在实际开发中还是挺有用的,特别是下面这些场景:

场景一:使用第三方模块化库

现在很多流行的 Java 库都模块化了,比如 Spring Framework、Hibernate 这些。如果你的项目还没模块化,用这些库的时候就可以用模块导入声明,省得一个个包地导。

// 假设你用的某个库已经模块化了,叫 com.example.mylib
import module com.example.mylib;  // 导入整个库模块

public class ThirdPartyLibraryExample {
    public void useLibrary() {
        // 直接使用库里的类,不用管具体包名
        SomeClass obj = new SomeClass();  // 库里的类
        AnotherClass another = new AnotherClass();  // 另一个类
        // 编译器会自动找到这些类在哪个包里
    }
}

场景二:简化标准库的使用

Java 标准库现在都是模块化的,java.base、java.desktop、java.sql 这些都是模块。用模块导入声明可以简化标准库的使用。

import module java.base;      // 基础类库
import module java.sql;       // 数据库相关
import module java.desktop;   // 桌面应用相关(如果需要)

public class StandardLibraryExample {
    public void databaseOperation() {
        // 直接用 SQL 相关的类
        Connection conn = DriverManager.getConnection("jdbc:mysql://...");  // 连接数据库
        Statement stmt = conn.createStatement();  // 创建语句
        ResultSet rs = stmt.executeQuery("SELECT * FROM users");  // 执行查询
        
        // 用基础库的工具类
        List<String> names = new ArrayList<>();  // 列表
        Map<String, Object> data = new HashMap<>();  // 映射
    }
}

场景三:迁移过程中的过渡

如果你的项目正在从非模块化向模块化迁移,模块导入声明可以作为一个过渡方案。你可以先用模块导入声明用那些已经模块化的库,等自己的代码慢慢模块化之后,再改成标准的模块依赖。

// 过渡期的代码:项目还没模块化,但要用模块化的库
import module java.base;
import module com.example.external.lib;  // 外部模块化的库

public class MigrationExample {
    // 你的代码还是传统的,没有 module-info.java
    // 但是可以用模块化的库
    public void useExternalLib() {
        ExternalClass obj = new ExternalClass();  // 外部库的类
        // 正常使用,不用管模块细节
    }
}

注意事项和限制

虽然这个特性挺方便的,但是也有一些需要注意的地方:

1. 只能导入已导出的包

模块导入声明只能导入模块通过 exports 导出的包。如果模块没有导出某个包,或者只通过 exports ... to 限制导出,那你就用不了。

// 假设有个模块 com.example.internal
// 它只导出了 com.example.public,没导出 com.example.internal

import module com.example.internal;  // 导入模块

public class LimitedAccessExample {
    public void useModule() {
        // 只能用导出的包
        com.example.public.PublicClass obj = new com.example.public.PublicClass();  // 可以
        
        // 这个用不了,因为包没导出
        // com.example.internal.InternalClass obj2 = new com.example.internal.InternalClass();  // 编译错误
    }
}

2. 编译时解析

模块导入声明是在编译时解析的,编译器会检查模块是否存在,以及是否有冲突。如果模块不存在或者有冲突,编译就会失败。

import module non.existent.module;  // 编译错误:模块不存在

public class CompileErrorExample {
    // 这个类编译不过
}

3. 运行时依赖

虽然你用了模块导入声明,但是运行时还是需要相应的模块在模块路径上。如果运行时找不到模块,程序就会运行失败。

# 编译时
javac -p <module-path> YourClass.java

# 运行时
java -p <module-path> -m your.module/YourClass

4. 性能考虑

模块导入声明不会影响运行时性能,它只是编译时的语法糖。编译器会把模块导入展开成具体的包导入,所以运行时的性能是一样的。

与传统导入的对比

咱们再详细对比一下传统导入和模块导入的区别:

传统导入方式

// 需要知道具体的包名
import java.util.Map;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.Collectors;
// ... 还有更多

public class TraditionalImport {
    // 代码
}

优点

  • 明确知道用了哪些包
  • 不会有隐式的依赖
  • IDE 支持好,自动补全准确

缺点

  • 导入语句多,代码冗长
  • 需要知道模块导出了哪些包
  • 添加新功能时可能要加很多 import

模块导入方式

// 一个声明搞定
import module java.base;

public class ModuleImport {
    // 代码
}

优点

  • 代码简洁,导入语句少
  • 不用关心模块内部结构
  • 添加功能时不用改 import

缺点

  • 不知道具体用了哪些包
  • 可能有隐式依赖
  • 冲突时需要显式解决

最佳实践

根据我的经验,用模块导入声明的时候,建议这么搞:

1. 优先用于标准库

标准库的模块结构稳定,用模块导入声明比较安全:

import module java.base;   // 基础库,最常用
import module java.sql;    // 数据库操作
// 按需导入其他标准模块

2. 第三方库要谨慎

第三方库的模块结构可能会变,用之前最好看看文档,确认一下模块名和导出情况:

// 确认库的模块名和导出情况后再用
import module com.example.verified.lib;  // 确认过的库

3. 处理冲突要及时

遇到冲突就马上解决,别拖,拖久了容易出问题:

import module java.base;
import module java.sql;

import java.sql.Date;  // 有冲突就显式导入,别犹豫

4. 大型项目考虑混合使用

大型项目可以混合使用,标准库用模块导入,自己的代码用传统导入:

import module java.base;           // 标准库用模块导入
import module java.sql;

import com.mycompany.util.helper;  // 自己的代码用传统导入,更明确
import com.mycompany.model.entity;

总结

JEP 511 的模块导入声明是个挺实用的特性,特别是对那些还在用传统方式写代码的项目。它让用模块化的库变得更简单,不用再一个个包地导了。

不过这个特性也不是万能的,有冲突的时候还是得手动解决,而且只能导入模块导出的包。用的时候要根据实际情况来,标准库可以放心用,第三方库就得谨慎点了。

总的来说,这个特性降低了模块系统的使用门槛,让更多开发者能享受到模块化的好处,是个不错的改进。如果你还没试过,建议试试,特别是那些 import 语句特别多的文件,用模块导入声明能清爽不少。

好了,今天就聊到这,有啥问题欢迎留言讨论。下次咱们聊聊 JEP 512,紧凑源文件和实例主方法,也是个挺有意思的特性。

本文章最后更新于 2025-11-27