1. 従来アプローチの問題点
「Classpath party」— みんなが同じキッチンにいる状態
Java(9 以前)では、アプリのコード、ライブラリ、依存関係がすべて巨大なひとかたまり、つまり classpath に積み上がっていました。ライブラリ間の境界は実質的に存在せず、classpath に入ったクラスは誰でも見つけて使えてしまいました。
名前衝突も起こります。2 つのライブラリが同じ完全修飾名のクラス、たとえば com.example.Util を含んでいる場合、「どれか一方」が選ばれてしまい、その問題にはランタイムでの奇妙な挙動を通じて気付くことになります。
典型例が dependency hell。あるライブラリはロガーのバージョン 1.2 を要求し、別のライブラリは 1.3 を要求。結果として両方が classpath に入ってしまう。JVM はどちらを拾うべきか分からず、あなたは謎の NoSuchMethodError を食らいます。
さらに、クラスが public であっても、ライブラリの内部用途のつもりであれば、本来は外部から使ってほしくありません。しかし他のコードからは普通に使えてしまい、その過程で何かを壊すことも。大規模プロジェクトは地雷原と化し、保守は冒険になっていました。
2. Java におけるモジュールとは?
モジュール: 穴の開いた箱にたとえると
Java 9 でモジュールシステム(JPMS)が登場しました。モジュールとは、クラスやパッケージを入れた箱で、あなたが自ら「穴」を開けます。つまり、外に見せるもの(exports)と中に隠すものを明示的に指定します。そう、たとえ public クラスでも、そのパッケージがエクスポートされていなければ他モジュールからは見えません。
形式的な定義
モジュールとは、関連するパッケージとクラスを束ねる論理的なコード分割単位です。各モジュールは次の点を明示的に宣言します。
- 何をエクスポートするか — 他のモジュールに公開するもの(exports)。
- 何をインポートするか — どのモジュールに依存するか(requires)。
モジュールの主な特性:
- 明確な境界。 外部に公開するものとしないものを厳密に定義。
- 明示的な依存関係。 明示的な requires がなければ他モジュールは「見えません」。
- 分離。 内部の詳細は完全に外部から隠蔽可能。
3. モジュール化の利点
改善されたカプセル化
モジュールレベルという新しい可視性が生まれました。クラスが public でも、そのパッケージがエクスポートされていなければ、そのクラスはモジュール内部でしか見えません。ライブラリの内側が外に「にじみ出る」ことはなくなります。
依存関係の明示
必要な依存関係は module-info.java に列挙します。コンパイラや IDE は、依存するモジュールの指定を忘れていれば事前に警告してくれます。
セキュリティと信頼性の向上
内部 API へのアクセス制限により、偶発的または意図的な介入を難しくします。これは大規模なライブラリやプラットフォームモジュールにとって極めて重要です。
「スリムな」JRE の作成が可能
jlink を使えば、必要なモジュールだけで最小限のランタイムを組み立てられます。クラウドや組み込みのシナリオでディスク容量やメモリを節約できます。
おまけ: 起動の高速化とサイズ削減
読み込まれるのはすべてのモジュールではなく、実際に使うモジュールだけです。アプリの起動は速くなり、消費リソースも減ります。
4. モジュールの適用先
Java 標準ライブラリで
Java 9 以降、プラットフォーム自体がモジュール化されています。例:
- java.base — 基本モジュール(すべてのプログラムに必須)。
- java.sql — データベース操作。
- java.xml — XML 操作。
XML を使わないなら、java.xml モジュールはあなたのランタイムにすら入りません。
大規模アプリケーションやライブラリで
多数のチームがモノレポジトリの一部を開発する企業システムでは事実上必須。モジュール化により衝突が減り、保守が容易になります。
自分のプロジェクトでも
pet プロジェクトでも、モジュールはアーキテクチャ思考の訓練になり、コードの整理に役立ち、「スパゲッティ」を避けられます。
5. 構文のクイックレビュー: module-info.java
最重要なのは module-info.java ファイル
ファイル module-info.java はモジュールのソースルートに置かれ、その境界と依存関係を宣言します。
module my.awesome.module {
exports com.example.api; // 公開するパッケージ
requires java.sql; // 標準モジュールへの依存関係
}
主要キーワード:
- module <名前> — モジュールを宣言する。
- exports <パッケージ> — パッケージを他のモジュールに公開する。
- requires <モジュール> — 別のモジュールへの依存関係を宣言する。
最小の例:
module com.myproject.core {
exports com.myproject.core.api;
}
依存関係を伴う例:
module com.myproject.app {
requires com.myproject.core;
requires java.sql;
}
追加機能(簡単に): リフレクション用には opens、サービス向けには uses と provides ... with ...(詳細は上級編で)。
6. 便利なポイント
モジュールは、クラスの「パスポート」のようなもの。 以前は、public というビザがあれば、どのクラスもアプリ内を自由に「旅」できました。今はモジュールの「パスポート」— パッケージのエクスポート(exports)も必要です。これがなければクラスは「自宅」に留まります。
Dependency hell は実在する用語です。 ClassNotFoundException: com.google.common.base.Strings を見たことがあるなら、まさにそこを通ってきました。モジュールは厳格な境界と依存関係で、こうした「厄介者」を追い払うためのものです。
今や Java 全体がモジュール化されています。 自分でモジュールを書かなくても、プラットフォームはすでに多数のモジュールに分割されています。次のコマンドを試してください:
java --list-modules
開発にどう影響するか
コードの可視性を明示的に管理でき、内部パッケージが「うっかり」誰にでも開かれることはなくなります。IDE とコンパイラはエラーを早期に検出します。依存関係の宣言を忘れればビルドは通りません。大規模システムの保守も簡単になります。誰が誰に依存しているか、変更で何が壊れるかを把握しやすくなるからです。
7. モジュール移行時のよくある誤り
エラー1: パッケージをエクスポートし忘れたが、クラスは public。 クラスを public にしていても、そのパッケージをエクスポートしていなければ、他のモジュールはそのクラスを使えません。コンパイラは「パッケージはモジュールによってエクスポートされていません」と知らせてくれます。
エラー2: requires で依存関係を宣言していない。 他モジュールの型を使っているのに、requires を module-info.java に追加し忘れているケースです。結果として「モジュールが必要なクラスを見つけられない」というコンパイルエラーになります。
エラー3: モジュール名の重複。 大規模プロジェクトで、2 つのモジュールが偶然同じ名前になってしまう。JVM はこれを許しません—名前を付け直し、一貫した命名規則を守りましょう。
エラー4: 標準モジュールを忘れている。 たとえば JDBC を使っているのに、requires java.sql; を追加していない。Java 8 では「見えていた」ものが、9 以降では見えません。
エラー5: 他モジュールの内部クラスを使おうとしている。 パッケージがエクスポートされていなければ、public クラスでも外部からは見えません。パッケージをエクスポートするか、API を「公開」レイヤーに切り出しましょう。
GO TO FULL VERSION