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:移除匯出
在 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 需要 B,而 B 又需要 A——就會出錯。避免在相依圖中造成循環。
GO TO FULL VERSION