Java モジュール・システムの要点
内容の確認日: 2026-10-04
この分野では、Java SE 9 で導入されたモジュール・システムの仕組みと、それを使うためのコマンドが問われます。module-info.java の各指示文の意味と、「モジュールの外から何が見えるか」のルールを正確に押さえることが中心です。コマンドのオプション (-p・-m など) も、短縮形まで問われます。
出題項目は「モジュール・システムの概要とモジュール型 JDK」「モジュールの宣言とモジュール間のアクセス」「モジュール化されたアプリケーションのコンパイルと実行」「無名モジュールおよび自動モジュールの使用」「jdeps の使用」「モジュール・システムにおける ServiceLoader の使用」の 6 つです。
モジュール型 JDK
Java SE 9 以降、JDK 自体が多数のモジュールに分割されています。java.lang・java.util・java.io などの基本的なパッケージを含む java.base は、すべてのモジュールが暗黙的に読み込むモジュールで、requires を書く必要はありません。java.sql (JDBC) や java.logging などは別のモジュールです。
java --list-modules # JDK のモジュールの一覧java --describe-module java.sql # モジュールの exports・requires などを表示 (短縮形は -d)クラスがどのモジュールに属しているかは、Class の getModule() で確かめられます。クラスパスから読み込まれたクラス (下の例の Main) は、名前のない無名モジュールに属します。
public class Main { public static void main(String[] args) { System.out.println(String.class.getModule().getName()); System.out.println(java.sql.Connection.class.getModule().getName()); System.out.println(Main.class.getModule().isNamed()); System.out.println(Main.class.getModule().getName()); }}java Main java.base java.sql false null
module-info.java の宣言
モジュールは、ソースのルートに置いた module-info.java で宣言します。主な指示文は次のとおりです。
module com.example.shop { requires java.sql; // java.sql モジュールを読み込む requires transitive com.example.util; // 自分を読み込むモジュールにも util を読み込ませる exports com.example.shop.api; // このパッケージを公開する exports com.example.shop.internal to com.example.admin; // 公開先を限定する (限定エクスポート) opens com.example.shop.model; // リフレクションでのアクセスを許可する}モジュールの外から使えるのは、exports したパッケージの public な型 (とその public・protected なメンバー) だけです。exports していないパッケージの型は、public でも使えません。exports はコンパイル時と実行時のアクセスを許可し、opens は実行時のリフレクション (private メンバーへのアクセスを含む) を許可します。モジュール全体を開くときは open module 名前 { ... } と書きます。
requires transitive を使うと、依存が推移します。lib が requires transitive util と書いていれば、lib を requires した app も util を読み込めます (transitive がなければ、app は自分で requires util を書く必要があります)。
主な制約は次のとおりです。
・exports・opens にはパッケージ名を 1 つずつ書く。ワイルドカード (*) は使えない
・requires に書くのはモジュール名 (パッケージ名ではない)
・requires による依存関係が循環してはいけない (コンパイルエラー)
・同じパッケージを複数のモジュールが含むこと (分割パッケージ) はできない
コンパイルと実行
モジュールは、module-info.java と一緒にコンパイルします。実行するときは、モジュールを置いたディレクトリ (または JAR) を --module-path (短縮形 -p) で指定し、--module (短縮形 -m) で モジュール名/メインクラスの完全修飾名 を指定します。
# コンパイル (出力先にモジュールのディレクトリを作る)javac -d mods/com.example.app src/com.example.app/module-info.java src/com.example.app/com/example/app/Main.java# 実行java -p mods -m com.example.app/com.example.app.Mainjar で JAR にまとめるときに --main-class を指定すると、実行時の -m ではモジュール名だけを書けば済みます。
jar --create --file mods/app.jar --main-class com.example.app.Main -C mods/com.example.app .java -p mods -m com.example.app--add-modules は、依存関係から自動では解決されないモジュールを、起動時に追加で読み込むオプションです。
無名モジュールと自動モジュール
モジュール化されていないコードとモジュールを組み合わせるために、2 種類の特別なモジュールがあります。
無名モジュール: クラスパスに置いたクラスや JAR は、すべて 1 つの無名モジュールにまとめられます。無名モジュールは、すべてのパッケージを公開し、すべてのモジュールを読み込みます。ただし名前がないので、名前付きモジュールから requires することはできません。
自動モジュール: module-info.class のない従来の JAR をモジュール・パスに置くと、自動モジュールになります。自動モジュールは、すべてのパッケージを公開し、すべてのモジュール (無名モジュールを含む) を読み込みます。名前付きモジュールから requires できます。名前は次のように決まります。
・JAR の META-INF/MANIFEST.MF に Automatic-Module-Name があれば、その名前
・なければ JAR のファイル名から決める。拡張子とバージョン番号 (-1.2 など) を除き、英数字以外の文字を . に置き換える (例: my-util-1.2.jar → my.util)
既存のライブラリを段階的にモジュール化するときは、まだモジュール化されていない JAR をモジュール・パスに置いて自動モジュールとして使う、という移行の方法 (ボトムアップ・トップダウン) がとれます。
jdeps による依存関係の分析
jdeps は、コンパイル済みのクラスファイルや JAR を静的に解析して、どのパッケージ・モジュールに依存しているかを表示するツールです。プログラムは実行しません。モジュール化の準備として、依存関係を調べるのに使います。
jdeps -s app.jar # 依存しているモジュールの概要 (-summary)jdeps --jdk-internals app.jar # JDK の内部 API (今後使えなくなる可能性がある) への依存を表示ServiceLoader とサービス
モジュール・システムでは、インタフェース (サービス) と、その実装 (サービス・プロバイダ) を別のモジュールに分け、実行時に実装を見つけて使えます。
// サービスのインタフェースを公開するモジュールmodule com.example.spi { exports com.example.spi; // Greeter インタフェースを含む}// 実装を提供するモジュールmodule com.example.impl { requires com.example.spi; provides com.example.spi.Greeter with com.example.impl.JapaneseGreeter;}// サービスを使うモジュールmodule com.example.app { requires com.example.spi; uses com.example.spi.Greeter;}サービスを使う側は、ServiceLoader.load(Greeter.class) で実装を探します。ServiceLoader は Iterable なので、拡張 for 文で実装を順に取り出せます。findFirst() は最初の実装を Optional で返し、実装が 1 つもなければ空の Optional になります (例外にはなりません)。stream() で Stream として扱うこともできます。
ServiceLoader<Greeter> loader = ServiceLoader.load(Greeter.class);for (Greeter g : loader) { System.out.println(g.greet());}Greeter first = loader.findFirst().orElseThrow();provides ... with に書く実装クラスには、public な引数なしのコンストラクタか、public static な provider() メソッドが必要です。使う側のモジュールは、実装のモジュールを requires しなくて構いません。
試験でよくある引っかけ
java.baseのrequiresjava.baseはすべてのモジュールが暗黙的に読み込むので、requiresは不要です。exportsしていないパッケージpublicなクラスでも、パッケージをexportsしていなければモジュールの外から使えません。requires transitivetransitiveを付けると、自分をrequiresしたモジュールにも依存先が読み込まれます。付けなければ推移しません。-pと-m-pは--module-path、-mは--moduleの短縮形です。-mにはモジュール名/クラス名を書きます。無名モジュールを
requiresクラスパス上のコード (無名モジュール) は、名前付きモジュールから
requiresできません。モジュール・パスに置けば自動モジュールになり、requiresできます。providesとuses実装を提供する側が
provides サービス with 実装、使う側がuses サービスを書きます。逆にしないよう注意。
問題で確認する
この分野の問題を、解説つきで読めます (登録不要)。
出典・参考
- Oracle University 日本「Java SE 17 Programmer II (1Z0-826-JPN) 試験」
- Java 言語仕様 (Java SE 17) 7.7「Module Declarations」
- Java SE 17 API ドキュメント「ModuleFinder」 (自動モジュールの名前の決め方)
- Java SE 17 API ドキュメント「ServiceLoader」
コード例は、試験の対象バージョンで実際にコンパイル・実行して結果を確認しています。