1. 包的导出:谁能看到你的代码?
在 Java 9 之前的世界,一切都很简单,但并不太安全。只要类是 public,任何人只要能访问 JAR 或 classpath 就能看到它。这会导致内部类,甚至整个包,可能被项目外部意外或有意地使用。是的,可以用 private 或 package-private 隐藏一些东西,但只要类是 public,它就对所有人可见。
有了模块之后,情况发生了变化。现在即使类声明为 public,如果其所在的包没有在 module-info.java 中显式通过 exports 导出,也不能从其他模块使用它。
实践中是什么样
假设你的项目结构如下:
my-app/
src/
core/
com/example/core/
CoreService.java
InternalHelper.java
module-info.java
app/
com/example/app/
Main.java
module-info.java
在文件 core/module-info.java 中写:
module core {
exports com.example.core;
}
这样,只有包 com.example.core 中的类对其它模块可见。位于其他包中的任何内容(例如 com.example.core.internal)都将不可见——即便那里有 public 类。这就是模块化封装。
迷你演示:“可见/不可见”
在 core 内部:
// com/example/core/CoreService.java
package com.example.core;
public class CoreService {
public void doWork() {
System.out.println("Work done!");
}
}
在 app 内部:
// com/example/app/Main.java
package com.example.app;
import com.example.core.CoreService;
public class Main {
public static void main(String[] args) {
CoreService service = new CoreService();
service.doWork();
}
}
如果你从 core/module-info.java 中移除 exports com.example.core; 这一行,编译时会得到错误:
error: package com.example.core is not visible
即便 CoreService 是 public!
2. 依赖导入:requires 与严格隔离
在 JPMS 中,不能随意使用来自其他模块的类。需要通过 requires 显式声明依赖。
示例
在文件 app/module-info.java 中:
module app {
requires core;
}
现在模块 app 可以使用模块 core 导出的所有内容。
如果你忘了写 requires core;,会得到编译错误:
error: package com.example.core is not visible
或者
error: cannot access CoreService
重要提示
- requires 在模块级别生效,而不是包级别。
- 你不能只“导入”某一个包——只能导入整个被导出的模块。
3. 对比:模块 vs public/private
模块引入了新的封装层级,位于类与包之“上”。
| 级别 | 控制什么? | 如何生效? |
|---|---|---|
| private | 类内部访问 | 仅在同一个文件内 |
| package-private(默认) | 包内访问 | 同一包内的所有类 |
| public | 对所有可见 | 任意位置的代码 |
| module | 模块之间的访问 | 仅导出的包 |
关键理念:
- 一个类可以是 public,但如果其包未导出,它只在模块内部可见。
- 通过 exports 导出包,好比为其他模块打开一扇“窗口”。
示例:隐藏的实现
// com/example/core/internal/SecretSauce.java
package com.example.core.internal;
public class SecretSauce {
public void addMagic() {}
}
如果你没有在 module-info.java 中导出包 com.example.core.internal,任何外部模块都无法使用这个类,即便它是 public!
4. 示例: core 与 app —— API 与实现
来看一个常见场景:模块 core 提供 API,模块 app 使用它。
结构:
core/
com/example/core/
CoreAPI.java
com/example/core/impl/
CoreImpl.java
module-info.java
app/
com/example/app/
Main.java
module-info.java
core/module-info.java:
module core {
exports com.example.core; // 仅限 API!
// 不导出 com.example.core.impl
}
app/module-info.java:
module app {
requires core;
}
com/example/core/CoreAPI.java:
package com.example.core;
public interface CoreAPI {
void doSomething();
}
com/example/core/impl/CoreImpl.java:
package com.example.core.impl;
import com.example.core.CoreAPI;
public class CoreImpl implements CoreAPI {
@Override
public void doSomething() {
System.out.println("Doing something!");
}
}
com/example/app/Main.java:
package com.example.app;
import com.example.core.CoreAPI;
// import com.example.core.impl.CoreImpl; // 这将导致编译错误!
public class Main {
public static void main(String[] args) {
// CoreImpl impl = new CoreImpl(); // 错误!该包未导出。
// 只能使用通过 API 可见的内容。
}
}
结果:
- 模块 app 只能看到 core 导出的内容。
- 实现(impl)被隐藏,即便其中的类是 public。
5. 实践:玩转导出与可见性
步骤 1:删除 export
在 core/module-info.java 中把这一行注释掉:
// exports com.example.core;
现在尝试编译项目。预期在模块 app 中出现编译错误——它将看不到 com.example.core 的类。
步骤 2:只导出 API
把 exports com.example.core; 这一行加回去,但不要导出 com.example.core.impl。试着在模块 app 中导入来自 impl 的类。你会再次得到编译错误——一切都很合理!
步骤 3:非导出包中的 public 类
在一个未导出的包中创建 public 类。尝试从其他模块使用它——行不通。这就是模块化封装的实际效果。
6. 在 IDE 中以及“形象比喻”的理解
- 尝试导入未导出包中的类时,IDE 会标红提示错误。
- 提示会说明该包未由模块导出。
- 为所需包添加 exports 后——错误就会消失。
示意图:访问级别
+---------------------+
| 模块 |
| (module-info.java) |
+---------------------+
|
v
+---------------------+
| 包 |
| (package, export) |
+---------------------+
|
v
+---------------------+
| 类 |
| (public/private) |
+---------------------+
|
v
+---------------------+
| 方法/字段 |
| (public/private) |
+---------------------+
7. 重要细节与特性
仅对“好友”导出:exports ... to
有时你只想把某个包导出给特定模块(例如测试或特殊扩展):
exports com.example.core.internal to my.special.module, my.test.module;
现在只有这些指定模块能看到该包。
可以导出多个包吗?
可以!只需添加更多 exports 行:
exports com.example.core;
exports com.example.core.api;
可以导出“一切”吗?
不可以。在 Java 的模块系统中,你必须始终显式声明要导出什么。这正是它的强大之处。
8. 使用模块化封装时的常见错误
错误 №1:以为 public 类总是可见。
如果包没有通过 module-info.java 导出,即便类是 public,对其他模块仍然不可见。这是新手最常见的“坑”。
错误 №2:忘了声明 requires。
如果你的模块使用了另一个模块的类,却没有写 requires,就会得到编译错误。务必显式描述依赖。
错误 №3:试图从两个模块导出同一个包。
在 JPMS 中,每个包只能由一个模块导出。如果违反此规则,编译器会阻止你。
错误 №4:结构不正确——module-info.java 放错了位置。
文件 module-info.java 必须放在模块源码的根目录,否则模块无法被识别。
错误 №5:模块之间的循环依赖。
如果模块 A requires B,而 B 又 requires A——就会报错。避免在依赖图中出现环。
GO TO FULL VERSION