最近のJavaはバージョンアップが早いですね。自分が初めて学んだものは1.8でしたが、すぐ9がでて今は13もリリースを目前としています。バージョンアップはバグ修正や性能の向上など良い面が多いためなるべく自分が使っているプログラムは常に最新のバージョンを維持したいですが、言語のバージョンが上がる度、何が変わったかを確認しすでに存在するコードを見直すのは簡単な問題ではいですね。
Javaはかなり歴史が長いので、現在のトレンドと比べてみると不便(パラダイムが変わったからという理由が多いと思いますが)な面が多いです。そして1.8が維持された期間が長かったのですが、そのため流行には遅れていますね。10になって型推論が導入されるなどトレンドを追いかけているような面もありますが、Kotlinのように同じくJVMを使う言語と比べてみるとまだ先が遠い印象はあります。
もちろん変化は肯定的なものであって、元の特徴を維持しながらもトレンドに合う書き方ができるようになったというのは、その言語を使えるユーザーのプールが広くなったとも評価できるでしょう。でも、全ての要素において「古いものと新しいもの共存」ができるわけではなさそうです。そういう場合はどちらを使うかを選択する必要がありますね。
今回のポストで話たいModuleがその代表的なものです。昔からの問題を改善するために導入されたものですが、結局は既存のコードに影響を与えてしまい、対応が必要となる部分です。最初は自分が書くコードでは考慮する必要がないものだろうと思っていたのですが、どうもそうはいかなかったです。なのでここでは、JavaのModuleが何であり、どんな問題を経験したかを述べたいと思います。
Project Jigsaw
ModuleはProject Jigsawという名で、1.7から導入を検討していたものらしいです。Moduleという名からわかるように、アプリケーションを起動する時読み込むライブラリー(Java内臓の)を選択することができるシステムです。1.8まではコマンドラインで起動するアプリケーションを作るとしても、基本的なシステムライブラリーであるSwingなどが含まれていたのですが、それを調整できるようになりました。要らないシステムライブラリーを除去するとアプリケーションのサイズも小さくなりますし、メモリーを節約できるというメリットもありますね。また、Javaの特徴でもあった「完璧にロードされるまでには時間がかかる」という問題も、このModuleの設定である程度解消できるようになりました。
そのほかにも、パッケージの「Publicすぎる問題」も、Moduleで解消できるようです。JavaのクラスはProtected宣言で同じパッケージでアクセスできるようにできますが、パッケージが細かく分けられた場合は同じライブラリーの中でもアクセスできませんでした。そういう場合はPublicで宣言するしかなかったですね。Publicで宣言されたクラスはライブラリーの中だけではなく、どこでもアクセスできるようになるため問題が生じる可能性もあります。ライブラリーを作りながらクライアントに使って欲しいクラスと使って欲しくないクラスを分けることが難しいことだったということです。これをModule設定により外部へ公開するクラスと、ライブラリー内部に向けて公開されるクラスで分けられるようになりました。
Moduleの実例
では、Public問題をModuleでどう解消できたかを、コードを持って説明します。まだ自分もModuleを積極的に使っているわけではないので基本となる部分だけですが、重要なポイントは以下の三要素だそうです。
- Name
- Exports
- Requires
まずNameは、Moduleそのものの名称を意味します。パッケージ名と同じ命名規則で書きます。次にExportsは、このModuleから外へ公開するパッケージのことを意味します。ModuleではPublicであっても、Exportsと明示されていないパッケージは外部からアクセスすることができません。そして最後にRequiresは他のModuleに対する依存関係を表します。
これらを実際のコードで書くと、以下のようになります。デフォルト・パッケージにmodule-info.java
として記述されます。(Java9以後のシステムライブラリーから確認できます)
// module-info.javaの書き方
module com.module.mylibrary {
exports com.module.mylibrary.api;
requires com.module.exlibrary;
}
Exportsの場合、公開対象を指定することができます。つまり、アクセスできるMobuleを指定することができるということです。
// exlibrary限定のPublic設定
module com.module.mylibrary {
exports com.module.mylibrary.api
to com.module.exlibrary;
}
Moduleはもちろん外部ライブラリーに対しても使うことができます。module-info.java
を作成する方法もありますが、Java9以前に作られたライブラリーの場合にはそれがない可能性が高いですね。このようにModuleかされていないライブラリーを含ませる必要がある場合はAutomatic Module
かUnnamed Module
の二つの方法からライブラリーを分けて使うことになります。両方自動的にModuleとして扱われるという面では同じで、全てのパッケージにアクセスできるという面では同じですが、前者はmodulepath
に属するものとして名前がある(Jarファイル名となります)ことに対して、後者はclasspath
に属するもので名称がないためRequiresで指定することができません。
Moduleでハマったところ
自分がModuleであった問題は、同じパッケージをもつ二つのライブラリーの競合によるものでした。問題が生じたのは、既存のプロジェクトにGradleのタスクを追加しようとしていたので原因でした。Gradleのタスクを作成する方法はbuild.gradle
に直接taskを作成することでもできますが、最初自分が参考にしていた方法(Gradleの公式文書に従いました)ではjava-gradle-plugin
というプラグインを含ませる方法でした。こうすると自動的にJavaのライブラリーが追加されて、Javaでプラグインを書けられるようになりますが、ここに含まれているライブラリーがJavaのシステムライブラリーと競合を起こしました。
元のプロジェクト(Java11を使っています)ではjavax.xml
をインポートしていて、これがJava9からはDeprecated
になり、最終的にJava11から除去されたらしいです。それがEclipse上ではUnnnamed Module
として読み込まれていたらしく、ちょうどjava-gradle-plugin
のパッケージにも同一名のパッケージが含まれていたので競合が起こったのです。そもそも除去されている扱いなので競合が生じるのがおかしいですが…エラ〜メッセージではThe package javax.xml.transform is accessible from more than one module: <unnamed>, javax.xml
と出力されていました。
似たような事例を参考にすると、二つの解決法が提示されていましたがどちらも自分のプロジェクトでは使えませんでした。module-info.java
を作成するとマルチプロジェクトとなっていてサブプロジェクト間のパッケージ依存関係まで考慮するには複雑な手続きが必要でしたし、Eclipseのモジュール依存関係設定からシステムライブラリーのjavax.xml
を除去すると、他にインポートしているjava.sql
がjavax.xml
に依存しているのでこちらも使えなくなるという問題がありました。
そしてリンクの文を読んでみると、最新のJava13までこの問題(自分のケースと完璧に一致しているとは言えませんが)は解決されてないというので、どうしようもない状態でした。java-gradle-plugin
はGradleで管理されているライブラリーなのでこちらからうかつに手を出すこともできませんでした。
結局どうしたらいいか
現時点では、外部ライブラリーを維持したまま競合だけを避ける方法はなさそうです。自分のModuleに対する理解がまだ足りてないことも原因かとは思いますが、結局はこのような事態が発生するとなるべく競合の原因となるライブラリーを除外するしか他の道はなさそうですね。便利さのために導入された新しい機能が、思わぬところで問題を起こしてしまうのはそう珍しいことでもないですが…3日ほど悩んだ私の選択は結局、そのライブラリーを使わないということしかなかったです。
もちろん、Moduleの問題なのでバージョンに対するこだわりがなければJavaを1.8に下げるという方法もあります。ただ1.8はいずれサポートが終わるはずで、これからもJavaのバージョンはどんどん上がっていくはずなのでいつかは直面することになるかもしれない問題ですね。どうかJava14ではこのような問題が起こらないことを祈ります。