※本記事は、“Bruce Eckel on Java modules, text blocks, and more” の翻訳記事です。
2022年6月17日| 9分読む
Project Jigsaw、三連二重引用符、try-with-resources、およびNullPointerExceptionを調べて、シリーズを締めくくります。
[このシリーズでは、Java 8以降にJava言語に追加された新機能について説明します。これは、2021年12月に公開された「On Java 8」の第2版に追加された資料から引用しています。これらの記事のソースコードおよびブック全体については、GitHubから、詳細なインストール手順とともに入手できます。シリーズ最終回となる今回は、これまでに説明していない機能をカバーしています。—編集部]
モジュール
JDK 9より前は、JavaプログラムにはJavaライブラリ全体が必要でした。つまり、シンプルなプログラムにも使用されることのない膨大な量のライブラリ・コードが搭載されていました。コンポーネントAを使用した場合、Aが依存する他のコンポーネントについてコンパイラに通知する言語サポートがありませんでした。この情報がないため、コンパイラはJavaライブラリ全体を含めるしかありません。
もう1つ、より重要な問題があります。パッケージ・アクセスは、そのパッケージ外での使用からクラスを効果的に隠すように見えますが、リフレクションを使用すれば回避できてしまいます。長年にわたり、一部のJavaプログラマは、直接使用することを意図していなかった低レベルのJavaライブラリ・コンポーネントにアクセスし、自分のコードをそれらの隠されたコンポーネントに結び付けてきました。つまり、Javaライブラリの設計者は、ユーザー・コードを壊すことなくこれらのコンポーネントを変更することが難しく、Javaライブラリの改良に大きな支障をきたしていました。
この2番目の問題を解決するには、外部プログラマがライブラリ・コンポーネントを完全に使用できないようにする必要がありました。
JDK 9では、これらの両方の問題を解決するモジュールの導入が決まりました。このシステムにより、Javaライブラリ設計者は、コードをきれいにモジュールに分割し、それらが依存するすべてのモジュールをプログラム的に指定できるようになり、どのコンポーネントがエクスポートでき、どのコンポーネントが完全に使用不可とするかを定義できるようになりました。
JDK 9の Project Jigsawは、JDKライブラリを約100のプラットフォーム・モジュールに分割しました。現在では、ライブラリ・コンポーネントを使用すると、そのコンポーネントのモジュールとその依存関係のみが取得され、使用しないモジュールが取得されることはありません。
非表示のライブラリ・コンポーネントを引き続き使用するには、明示的に救援機能を有効にする必要があります。そうすることで、ユーザーは意図したライブラリ設計に反していることがすぐにわかり、その非表示コンポーネントの更新(または完全に削除)によって発生しうる障害は、ユーザーの責任となります。
モジュールの探索:新しいコマンドライン・フラグを使用して、新しいモジュール・システムを確認できます。使用可能なすべてのモジュールを表示するには、コマンド・プロンプトで次を実行します:
java --list-modules
このコマンドを実行すると、次のように出力されます。
java.base@11 java.compiler@11 java.datatransfer@11 java.desktop@11 java.instrument@11 java.logging@11 java.management@11 java.management.rmi@11 java.naming@11 java.net.http@11 ...
@11は情報提供のみを目的としており、使用されているJDKのバージョンを示します。モジュールを参照するときには含まれません。モジュール(例えばbaseモジュール)の内容を確認するには、コマンド・プロンプトで次を実行します。
java --describe-module java.base
次が表示されます。
java.base@11 exports java.io exports java.lang exports java.lang.annotation exports java.lang.invoke exports java.lang.module exports java.lang.ref exports java.lang.reflect exports java.math exports java.net exports java.net.spi exports java.nio ... uses java.text.spi.DateFormatSymbolsProvider uses sun.util.locale.provider.LocaleDataMetaInfo uses java.time.chrono.Chronology uses java.nio.channels.spi.AsynchronousChannelProvider uses sun.text.spi.JavaTimeDateTimePatternProvider ... provides java.nio.file.spi.FileSystemProvider with jdk.internal.jrtfs.JrtFileSystemProvider ... qualified exports sun.security.timestamp to jdk.jartool qualified exports sun.security.validator to jdk.jartool qualified exports jdk.internal.org.xml.sax to jdk.jfr qualified exports sun.security.provider.certpath to java.naming qualified exports sun.security.tools to jdk.jartool ... contains sun.text contains sun.text.bidi contains sun.text.normalizer contains sun.text.resources contains sun.text.resources.cldr contains sun.text.spi contains sun.util contains sun.util.calendar contains sun.util.locale contains sun.util.resources.cldr contains sun.util.spi
これにより、モジュールがJavaライブラリに対して有効にするコンポーネント構造の種類がわかります。
自分のアプリケーションにモジュールを使用すべきですか?:独自のアプリケーションにモジュール・システムを使用することは確かに可能ですが、ほとんどのプロジェクトでは、利点が労力を上回らないようです。モジュールを使用せずに引き続きアプリケーションを作成することができ、モジュール化された標準のJavaライブラリの恩恵も受けられます。
ただし、大規模で複雑なライブラリを作成する場合は、モジュール・システムを使ったライブラリの実装方法を学習するために労力を費やすことをお勧めします。ただし、大規模なライブラリ以外の場合は、独自のモジュールを定義および使用せずに構築すれば十分です。
テキスト・ブロック
JEP 378でJDK 15にテキスト・ブロックが追加され、複数行テキストを簡単に作成できるようになりました。三連二重引用符は、改行を含むテキストのブロックを示します。
// strings/TextBlocks.java
// {NewFeature} Since JDK 15
// Poem: Antigonish by Hughes Mearns
public class TextBlocks {
public static final String OLD =
"Yesterday, upon the stair,\n" +
"I met a man who wasn't there\n" +
"He wasn't there again today\n" +
"I wish, I wish he'd go away...\n" +
"\n" +
"When I came home last night at three\n" +
"The man was waiting there for me\n" +
"But when I looked around the hall\n" +
"I couldn't see him there at all!\n";
public static final String NEW = """
Yesterday, upon the stair,
I met a man who wasn't there
He wasn't there again today
I wish, I wish he'd go away...
When I came home last night at three
The man was waiting there for me
But when I looked around the hall
I couldn't see him there at all!
""";
public static void main(String[] args) {
System.out.println(OLD.equals(NEW));
}
}
/* Output:
true
*/
({NewFeature} コメント・タグによって、JDK 8を使用するGradleビルドから、この例は除外されます。.)
OLD の方は、複数の行を処理する従来の方法を示しており、多くの改行文字と+記号を使用しています。一方、 NEW では、テキスト・ブロックを使用してこれらを排除し、より優れた読みやすい構文を実現しています。
三連二重引用符(“””)で開始された後の改行が自動的に削除され、ブロック内の共通のインデントが取り除かれているため、NEWの結果にはインデントがないことに注意してください。インデントを保持する場合は、次のように最後の”””を左に移動して目的のインデントを生成します。:
// strings/Indentation.java
// {NewFeature} Since JDK 15
public class Indentation {
public static final String NONE = """
XXX
YYY
"""; // No indentation
public static final String TWO = """
XXX
YYY
"""; // Produces indent of 2
public static final String EIGHT = """
XXX
YYY
"""; // Produces indent of 8
public static void main(String[] args) {
System.out.print(NONE);
System.out.print(TWO);
System.out.print(EIGHT);
}
}
/* Output:
XXX
YYY
XXX
YYY
XXX
YYY
*/
テキスト・ブロックをサポートするために、新しく formatted() メソッドが String クラスに追加されました。
// strings/DataPoint.java
// {NewFeature} Since JDK 15: formatted()
// Since JDK 16: record
record DataPoint(String location, Double temperature) {
@Override public String toString() {
return """
Location: %s
Temperature: %.2f
""".formatted(location, temperature);
}
public static void main(String[] args) {
var hill = new DataPoint("Hill", 45.2);
var dale = new DataPoint("Dale", 65.2);
System.out.print(hill);
System.out.print(dale);
}
}
/* Output:
Location: Hill
Temperature: 45.20
Location: Dale
Temperature: 65.20
*/
formatted() はメソッドであり、String.format()のような個別の static function like String.format()関数ではありません。そのため、どんな Stringに対しても最後に追加するだけで済むため、よりすっきりでわかりやすくなります。テキスト・ブロックの結果は通常の Stringであるため、他のStringで実行できるすべての操作を実行できます。
NullPointerExceptionレポートの改善
NullPointerException エラーに関するフラストレーションの問題の1つは、生成された情報の不足でした。メッセージはJDK 14でJEP 358を使用してより有益になりましたが、有用なメッセージはデフォルトで有効になっていませんでした。JDK 15では、 JDK-8233014はこれらの有用なメッセージをデフォルトとしてオンにしました。
次の例では、NULLがオブジェクトのチェーンに挿入され、エラーが発生します。
// exceptions/BetterNullPointerReports.java
// {NewFeature} Since JDK 15
// Since JDK 16: record
record A(String s) {}
record B(A a) {}
record C(B b) {}
public class BetterNullPointerReports {
public static void main(String[] args) {
C[] ca = {
new C(new B(new A(null))),
new C(new B(null)),
new C(null),
};
for(C c: ca) {
try {
System.out.println(c.b().a().s());
} catch(NullPointerException npe) {
System.out.println(npe);
}
}
}
}
この改善以前は、同様のコード( record 参照をより冗長な class に置き換えたもの)では、ほとんどの情報が得られません。
null java.lang.NullPointerException java.lang.NullPointerException
ただし、JDK 15以降を使用している場合は、次のように表示されます。:
null java.lang.NullPointerException: Cannot invoke "A.s()" because the return value of "B.a()" is null java.lang.NullPointerException: Cannot invoke "B.a()" because the return value of "C.b()" is null
この変更により、 NullPointerException エラーの理解と解決がかなり容易になります。
try-with-resourcesで有効なfinal変数
元のtry-with-resourcesでは、すべての管理対象変数がリソース仕様ヘッダー( tryのカッコ付きリスト)内に定義されている必要があります。いくつかの理由でJavaチームは、これをときどきぎこちないと考えていました。JDK 9では、これらの変数が明示的にまたは実質 finalである場合、tryの前にこれらの変数を定義する機能が追加されました。古いtry-with-resources構文とJDK 9構文(オプション)の比較を次に示します。
// exceptions/EffectivelyFinalTWR.java
// {NewFeature} Since JDK 9
import java.io.*;
public class EffectivelyFinalTWR {
static void old() {
try (
InputStream r1 = new FileInputStream(
new File("TryWithResources.java"));
InputStream r2 = new FileInputStream(
new File("EffectivelyFinalTWR.java"));
) {
r1.read();
r2.read();
} catch(IOException e) {
// 例外処理
}
}
static void jdk9() throws IOException {
final InputStream r1 = new FileInputStream(
new File("TryWithResources.java"));
// 実質 final:
InputStream r2 = new FileInputStream(
new File("EffectivelyFinalTWR.java"));
try (r1; r2) {
r1.read();
r2.read();
}
// r1 と r2 はまだスコープ内にあります
// どちらかにアクセスすると例外がスローされます。:
r1.read();
r2.read();
}
public static void main(String[] args) {
old();
try {
jdk9();
} catch(IOException e) {
System.out.println(e);
}
}
}
/* Output:
java.io.IOException: Stream Closed
*/
The jdk9()は、 throws IOExceptionを指定して例外を渡します。これは、 old()で try 内にあったr1およびr2の定義がtry内に存在しなくなったためです。例外をキャッチできないことが、機能の改善が必要な理由の1つです。
jdk9()の最後にあるように、try-with-resourcesによってリリースされた変数を参照することもできます。コンパイラはこれを許可しますが、 tryブロック外でr1またはr2にアクセスすると例外が発生します。