1. module-info.java とは何か、どこにあるのか?
Java のモジュールは常にファイル module-info.java から始まります。これはいわばモジュールの「パスポート」で、モジュール名、エクスポートするパッケージ、他モジュールへの依存関係を宣言します。
どこにある?
ファイル module-info.java は、モジュールのソース・ルート直下(ソースフォルダのルート)に置きます。例えば、プロジェクト構成が次のような場合:
project-root/
└── src/
└── my.module.name/
├── module-info.java
└── com/
└── example/
└── api/
└── MyClass.java
ここで my.module.name はモジュール名です(命名規則は後述)。
このファイルがないと、モジュールはモジュールとは見なされません!
もし存在しなければ、たとえフォルダやクラスが大量にあっても、ただの従来型の Java プロジェクトということになります。
2. module-info.java の構文: 基本要素
まずは最もシンプルなファイル例を見てみましょう — 怖くありません、ほんとです!
module my.module.name {
exports com.example.api;
requires java.sql;
}
部分ごとに見ていきましょう:
module my.module.name { ... }
これは my.module.name という名前のモジュールの宣言です。モジュール名は一意の識別子で、一般的にはルートパッケージ名と一致させます(例: com.example.app)。
豆知識: モジュール名を java.base にするとコンパイラは機嫌を損ねます。標準モジュールを置き換えようとするのはやめましょう。
exports com.example.api;
この行は「私は、com.example.api パッケージ内のものを公開します」と示しています。このパッケージ内で public として宣言されたものは、他モジュールから見えるようになります。それ以外はモジュール内限定です。
requires java.sql;
ここではコンパイラに対して正直に「このモジュールは標準モジュール java.sql を必要とします」と宣言しています。これを宣言しないと、そのモジュールのクラスは使えません。
追加のキーワード(知っておくと便利)
- opens <package>; — リフレクション用にパッケージを開きます(例: Jackson のようなシリアライゼーション系ライブラリ)。
- uses <service-interface>; — モジュールがあるサービス(インターフェース)を利用することを示します。
- provides <service-interface> with <implementation-class>; — モジュールがそのサービスの実装を提供することを示します。
本講義では exports と requires に焦点を当てます。これらは学習用・本番の大半のプロジェクトで必要になります。
3. module-info.java の例
例 1. 最小のモジュール
module com.example.hello {
exports com.example.hello.api;
}
- このモジュールはパッケージ com.example.hello.api のみをエクスポートします。
- com.example.hello.internal のような場所にあるものは、たとえ public クラスがあっても他モジュールからは見えません。
例 2. 依存関係を持つモジュール
module com.example.dbclient {
exports com.example.db.api;
requires java.sql;
}
JDBC を使えるのは、java.sql への依存関係を正しく宣言しているからです。
例 3. 複数のエクスポート対象パッケージ
module com.example.library {
exports com.example.library.api;
exports com.example.library.utils;
}
エクスポートするパッケージはいくつでも構いません(ただし何でもかんでもエクスポートするのはやめましょう。モジュール化の意義が失われます!)。
4. 制約とルール
モジュール名
- 一般的にルートパッケージ名と一致させます(例: com.example.app)。
- 標準モジュール名(java.base、java.sql など)と同じにしてはいけません。
- 空白や特殊文字を含めない、数字で始めない、などの制約があります。
- 推奨: 衝突を避けるため、組織やプロジェクトの逆ドメイン名を使いましょう。
1 モジュールにつき 1 つの module-info.java
1 つのモジュールにこのファイルは 1 つだけです。2 つあるとコンパイラが「モジュール騒動」を起こします。
同一パッケージのエクスポートは 1 つのモジュールからのみ
同じパッケージを 2 つの異なるモジュールからエクスポートすることはできません。1 つの名前にパスポートが 2 つあるようなものです — 国は認めません。
モジュール内のパッケージ
実際にそのモジュールのソース構成内に存在するパッケージだけをエクスポートできます。存在しないパッケージをエクスポートしようとするとコンパイルエラーになります。
5. 実践: プロジェクトで module-info.java を作成する
次の内容のシンプルなプロジェクトがあるとします:
project-root/
└── src/
└── com.example.greetings/
├── module-info.java
└── com/
└── example/
└── greetings/
├── api/
│ └── Greeter.java
└── internal/
└── SecretSauce.java
ステップ 1. module-info.java を作成する
module com.example.greetings {
exports com.example.greetings.api;
}
ステップ 2. 別モジュールで internal パッケージのクラスを使ってみる
2 つ目のモジュール com.example.app が SecretSauce にアクセスしたいとします:
module com.example.app {
requires com.example.greetings;
}
import com.example.greetings.internal.SecretSauce; // エラー!
結果:
コンパイラは「パッケージ com.example.greetings.internal はモジュール com.example.greetings によってエクスポートされていません」と言うでしょう。たとえクラス SecretSauce が public でも、他モジュールからはアクセスできません。
これがモジュールレベルの本当のカプセル化です。
ステップ 3. requires を宣言しない場合
もし com.example.app で requires com.example.greetings; を書かずに、com.example.greetings.api のクラスを使おうとすると、コンパイラは次のエラーを出します:
package com.example.greetings.api is not visible
6. module-info.java を扱うときのよくあるミス
エラー 1: モジュール名とプロジェクト構成の不一致。
モジュールを com.example.app と名付けたのに、フォルダ構成が src/main/java/app のようになっていると、コンパイラは意図を理解できません。モジュール名は通常ルートパッケージと一致し、フォルダもそれを反映している必要があります。
エラー 2: 片っ端からエクスポートする。
他モジュールから見える必要があるものだけをエクスポートしましょう。「その方が簡単だから」といって exports com.example; のように大ざっぱに書くのはやめましょう。カプセル化が壊れます。
エラー 3: requires の書き忘れ。
他モジュールや標準ライブラリ(例: java.sql)のクラスを使うのに、依存関係を宣言し忘れるとコンパイルエラーになります。
エラー 4: exports に存在しないパッケージ。
exports com.example.foo; と書いたのに、そのパッケージが存在しない場合 — コンパイラは「空気をエクスポートしています」と言うでしょう。
エラー 5: 非エクスポートパッケージ内の public クラス。
クラスが public であっても、そのクラスが属するパッケージをエクスポートしていなければ、そのクラスはモジュール内でしか見えません。エラーではありませんが、初心者が驚きがちなポイントです。
エラー 6: 同一モジュール内に複数の module-info.java。
1 つのモジュールには module-info.java は 1 つだけです。2 つあるとコンパイラはプロジェクトをビルドできません。
GO TO FULL VERSION