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. 実践:exports と可視性を試してみる
ステップ1:exports を削除する
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