この記事について
Java21が2023年9月19日に正式リリースされました。
そこで、Java好きな筆者が記事執筆時点1での最新バージョンであるJava21の新機能を調べ、その中で一部を抜粋し紹介します。
動機
筆者がJava21の新機能を調べようと思ったのは、以下の理由です。
- 単純にJavaが好きだから
- Javaの専門家のミドルウェアとしての設計・開発の思想を知り新たな学びを得たいと思ったから
本記事は、その調査を通じて得た知見を共有することを目的で執筆しております。
本記事の想定する読者
本記事は、以下のような方を読者として想定しています。
- Java21の新機能に興味がある人
- Java専門家のプロダクトとしての設計思想に触れたい人
後者について、OpenJDKの開発には現場経験が豊富なエンジニアが携わっています。その方々が現場で苦労した経験やあったら嬉しいと思った経験に基づいて新機能が開発されているため、それらの経緯を知ることで、その思想の一端に触れられるかと思います。
前提
Javaでは機能提供の形態がいくつ定義されています。
Javaの新機能の提供形態については、Oracleのブログ記事に詳しく書いてありますが、ざっくりいうと以下の形態です。
形態 | 概要 |
---|---|
feature | 正式にリリースされて調整が済んでいる機能 |
preview | まだ調整の余地がある機能(今後機能が変わる可能性あり) |
incubator | 新しいAPIやJDKツールになる可能性があるもの |
previewの機能は試験運用版の機能です。開発者側としてはある程度機能が完成しているため、試験運用版としてリリースされています。利用者からのフィードバックを経て修正と改善を行って、最終的にはfeature(正式リリース)されることを目指しているといったものです2。
incubatorは、改善や安定化が行われてから将来的には正式な機能への組み込みを目指しているものです。まだ開発途中段階の機能と思っておいてください3。
上記のように、previewやincubatorはまだ正式なリリースがされていない機能であり、今後仕様が変わる可能性がありますので、実際の開発に使う際はその点を留意しておく必要があります。
Java21の新機能
今回のJava21では、主要な機能として以下のものが新規に追加されました。
Feature ID | 機能 | 提供形態 | 種別 |
---|---|---|---|
430 | String Templates | Preview | 言語のアップデートと改善 |
431 | Sequenced Collections | APIの拡張 | |
439 | Generational ZGC | パフォーマンスのアップデート | |
440 | Record Patterns | 言語のアップデートと改善 | |
441 | Pattern Matching for switch | 言語のアップデートと改善 | |
442 | Foreign Function & Memory API | Third Preview | Project Panamaのプレビュー機能 |
443 | Unnamed Patterns and Variables | Preview | 言語のアップデートと改善 |
444 | Virtual Threads | Project Loomの機能 | |
445 | Unnamed Classes and Instance Main Methods | Preview | 言語のアップデートと改善 |
446 | Scoped Values | Preview | Project Loomの機能 |
448 | Vector API | Sixth Incubator | Project Panamaのプレビュー機能 |
449 | Deprecate the Windows 32-bit x86 Port for Removal | その他 | |
451 | Prepare to Disallow the Dynamic Loading of Agents | その他 | |
452 | Key Encapsulation Mechanism API | APIの拡張 | |
453 | Structured Concurrency | Preview | Project Loomの機能 |
Javaのリリースについて、Oracle社はバージョン17以降、2年ごとの頻度でLTSバージョンをリリースするというロードマップ4を定めているので、最新のLTSバージョンを使いこなせるよう、新機能はnon-LTSバージョンも含めて随時チェックしてみましょう!
ちなみに、Java22も絶賛開発中なので、気になる人はGitHub上のJDKポジトリで最新の開発状況を追ってみるとよいかもしれません。
Java21(JDK)の導入手順
Java21をLinuxの環境で実行する方法を紹介します。
(この章ではJDKをインストールする手順を説明しているので、記事の内容を読むだけでよければスキップしてください!)
導入では以下の二つの手順を実施します。
- JDKのダウンロード
- 実行ファイルのパスを設定
導入環境
今回Java21を導入する環境は以下の通りです。
情報 | |
---|---|
OS | Ubuntu20.04(WSL) |
プロセッサ | Intel Corei5-1135G7 |
メモリ | 16.0GB |
JDKのダウンロード
Java21のJDKを使うにあたり、apt
などのパッケージ管理ツールで提供しているディストリビューションはまだ多くないと思われるので、今回は外部サイトからダウンロードする方法を紹介します。
まず、公式サイトからJDKをダウンロードしてきましょう。
WSL上で処理を行うため、Linux向けのJDKを持ってきましょう。
以下のコマンドを実行します。
# ファイルをダウンロード
$ wget https://download.oracle.com/java/21/latest/jdk-21_linux-x64_bin.tar.gz
# ダウンロードした圧縮ファイルを解凍
$ tar xvzf ./jdk-21_linux-x64_bin.tar.gz
# ファイルが解凍されたか確認
$ ls
jdk-21.0.1 jdk-21_linux-x64_bin.tar.gz
実行ファイルのパスを設定
今回の機能検証で用いるのは以下のバイナリです。
- /path/to/dir/jdk-21.0.1/bin/javac(コンパイル用)
- /path/to/dir/jdk-21.0.1/bin/java(実行用)
実行する度にパスを入力するのは面倒なので、通常通りにコマンド実行できるようにしましょう。
今回はaliasコマンドを使ってみます。
# コンパイル用のコマンドパスを設定
$ alias javac=/path/to/dir/jdk-21.0.1/bin/javac
# 実行用のコマンドパスを設定
$ alias java=/path/to/dir/jdk-21.0.1/bin/java
次に、正しく設定できたか確認してみましょう。
$ javac --version
javac 21.0.1
$ java --version
java 21.0.1 2023-10-17 LTS
Java(TM) SE Runtime Environment (build 21.0.1+12-LTS-29)
Java HotSpot(TM) 64-Bit Server VM (build 21.0.1+12-LTS-29, mixed mode, sharing)
うまく動いているようですね!
そういえば、コマンド実行でバージョンを確認して気づいたのですが、Java21がリリースされてから一か月後の10月17日にパッチが当たったのですね。
Java21には試験導入された機能もいろいろあるので、これからも定期的にパッチが充てられていくのでしょう。動向は要確認です。
個人的に印象に残った機能
さて、いよいよ本題です。
筆者が個人的に気になっている新機能を抜粋して紹介させていただきます。
Java21では色々な新機能が提供されており、これまでのコンパイラでは検知できなかったパターンのバグを検知できるようになったものや、パフォーマンスを向上させるものなどがあります。
本記事では具体的な利用方法も併せて紹介させてもらいたくございます。
Java21の機能を試す際の留意点
新機能の調査開始時(2023/10/27時点)、IntelliJとVSCodeを調べてみたのですが、どうやらまだJava21にエディタが対応していなさそうでした。
Eclipseについては、記事執筆時点1で最新版のEclipse IDE for Java Developers - 202309はJava20まで対応しており、Java21についてはまだ未対応のようです。
そのため、エディタからエラーだと怒られちゃうかもしれませんが、コンパイルと実行は可能です。気にせず書いていきましょう。
JEP431:Sequenced Collections
Java21で、コレクションオブジェクトに共通インターフェースとしてSequencedCollectionsが導入されました5。
本機能が実装されたことで、CollectionやMapといった繰り返し構造を持つクラスについてメソッドの呼び出し方が統一されました。
例えば、Java20までのバージョンにおいて、Iterableなオブジェクトから最後のデータを取得したいとします。
その時、以下のような方法で実現することになります。
list.get(list.size() - 1); // List構造を持つオブジェクトで最後のデータを取得したいとき
deque.getLast(); // Deque構造を持つオブジェクトで最後のデータを取得するとき
sortedSet.last(); // SortedSet構造を持つオブジェクトで最後のデータを取得するとき
上記のコードを見るとわかる通り、それぞれのクラスごとにメソッド名や引数が違っています。
このように、同じような動作をしているにもかかわらず各クラスごとに独自の方法で設計しており(そもそも用意していない場合もあり)、利用者側は利用しているオブジェクトごとに呼び出し方を調べる必要がありました。
今回の新機能のおかげで、これらの呼び出し方が統一されました。
list.getLast(); // List構造を持つオブジェクトで最後のデータを取得したいとき
deque.getLast(); // Deque構造を持つオブジェクトで最後のデータを取得するとき
sortedSet.getLast(); // SortedSet構造を持つオブジェクトで最後のデータを取得するとき
メソッドが統一されたので、Java21以降を利用する人は「getLast()
メソッドを使えば最後の要素を取得できるんだ!」とさえ知っておけばデータを取得できるようになったわけですね。
ちなみに、Java21で追加されたSequensedCollection
は以下のメソッドです6。
// 新規メソッド
SequencedCollection<E> reversed();
// Dequeで提供されていたメソッド
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
reversed()
メソッドは今回初めて導入されました。名前や戻り値の型でお察しの方も多いと思いますが、オブジェクトが保持するデータ順番を反転させて返すメソッドです。
これまでは、Collections
のクラスメソッドであるreverse(List)
メソッドで反転させる方法がありましたが、これはオブジェクト内のデータ順序を反転して書き換えるものでした。新しいメソッドでは、ビューを提供する仕組みなため、順番を並び替えたものだけ見たいときに活用できそうですね。
あとは、Deque
クラスにだけ用意されていた便利なメソッドをSequencedCollection
に移動させて、別のクラスでも使えるようになりました。
ほかにもMap
でもSequencedMap
として機能を用意しています(本記事では割愛)7。
この新機能からの学び
この新機能から学べる設計思想として、共通規格としてインターフェースを用意して仕様を統一することで、プログラマーの学習コストを下げられるといったことがあります。
コレクションの端点のデータを参照したり追加したりといった処理は、プログラマーであれば一度は記述する処理であり、そのやり方はオブジェクトによって差異がありました。
今回の新機能により、共通のメソッドがAPIレベルで提供されたことから、API利用者側は別のオブジェクトを使う処理でも同じ要領で処理を実装すればよいため、内部ロジックに詳しくなる必要がなくなるわけです!
JEP441:Pattern Matching for switch
Java21では、switchを用いてパターンマッチングができるようになりました8。
本機能は、今回追加された機能の中でもかなりインパクトが大きいものだと思います。
皆さんに一つ質問です。
「Javaにおいて、interfaceに対応したいくつかの実装クラスがあり、実装クラスのインスタンスをパターンごとに場合分けして処理したいときはどんな記述をしますか?」
BingAIに同じ質問を投げかけたところ、以下のような回答でした。
Javaにおいて、interfaceに対応したいくつかの実装クラスがあり、実装クラスのインスタンスをパターンごとに処理を分けて実行する場合、以下のような記述をします。
public void processVariable(InterfaceName interfaceInstance) { if (interfaceInstance instanceof ClassName1) { ClassName1 className1 = (ClassName1) interfaceInstance; // パターン1の処理 } else if (interfaceInstance instanceof ClassName2) { ClassName2 className2 = (ClassName2) interfaceInstance; // パターン2の処理 } else if (interfaceInstance instanceof ClassName3) { ClassName3 className3 = (ClassName3) interfaceInstance; // パターン3の処理 } else { // その他の処理 } }
この例では、processVariableメソッドが定義されており、引数としてInterfaceName型のinterfaceInstanceが渡されます。if-else文を使用して、引数がどのクラスのインスタンスであるかを判別し、異なる処理を実行することができます。この例では、引数がClassName1クラスのインスタンスである場合はパターン1の処理を、引数がClassName2クラスのインスタンスである場合はパターン2の処理を、引数がClassName3クラスのインスタンスである場合はパターン3の処理を実行します。引数が上記以外のクラスのインスタンスである場合は、その他の処理を実行します。
ただし、この例はあくまでも一例であり、実際にはインターフェースやクラスの実装方法によって異なる記述方法があります。
要約すると、if-else
文とinstanceof
演算子を使って処理を場合分けするというものでした。私もこの実装方法になるのかな~と思っています。
今回の新機能では、switch式によるパターンマッチングが提供されました。
具体的には以下のような書き換えが可能になります。
public void processVariable(InterfaceName interfaceInstance) {
switch (interfaceInstance) {
case ClassName1 className1 -> // パターン1の処理
case ClassName2 className2 -> // パターン2の処理
case ClassName3 className3 -> // パターン3の処理
default -> // その他の処理
}
}
このように、switchに基づくパターンマッチング機能が追加されたおかげで、if-else
やinstanceof
を用いることなくシンプルにコードを書けるようになりました。
...とだけ説明しても、if-elseをswitchに置き換えただけで、あまりメリットがないじゃないか!と感じる人がいるかもしれません。
しかし、私が今回機能を試していて大きなメリットだと感じた場面があったので、それについて説明させてください!
以下のようなケースがあるとしましょう。
「とあるObject型の変数が与えられたとき、その変数が文字列型であればそのまま標準出力し、変数が何らかの数字であれば"数字"とだけ標準出力し、変数が整数であれば"整数"とだけ標準出力するようなJavaコードを記述しなさい。」
この命令をBingAIに与えると以下のような回答が得られました。
与えられた要件を満たすJavaコードは以下のようになります:
if (variable instanceof String) { System.out.println(variable); } else if (variable instanceof Number) { System.out.println("数字"); } else if (variable instanceof Integer) { System.out.println("整数"); }
このコードでは、instanceof演算子を使用して、変数がどの型に属するかを判定しています。変数が文字列型であれば、そのまま標準出力します。変数が何らかの数字であれば、"数字"とだけ標準出力します。変数が整数であれば、"整数"とだけ標準出力します。
なお、このコードは与えられた要件を満たすように記述されていますが、実際の開発においては、変数の型が上記の3つ以外の場合も考慮する必要があるかもしれません。
偉大なるGPT-4から上記のような回答を得られました。実際、このように書く人もいるかもしれませんね。
さて、このコードには問題点があります。それは何でしょうか。
答えは、三番目のif条件が検証されることがない(=到達不能コード)ということです。
というのも、IntegerはNumberの子クラスであるためvariable
がInteger型であるときは二番目の条件分岐が必ずtrueになります。そのため、三番目の条件分岐は実行されることがなく、variableは整数にもかかわらず実行されないという問題が生じます。
さて、このコードをそのままJava21の新機能で置き換えてみましょう。
switch (variable) {
case String s -> System.out.println(s);
case Number n -> System.out.println("数字");
case Integer i -> System.out.println("整数");
}
このコードを実行してみると、以下のようなエラーが返されます。
$ java JEP440
JEP441.java:10: error: this case label is dominated by a preceding case label
case Integer i -> System.out.println("整数");
^
1 error
要するに、先に宣言しているケース文がこのケース文の条件を包括しているから実行されることがないよというエラーです。
このように、パターンマッチングを利用することで、開発者が意図した条件分岐を実行してくれるようになるので、書き間違えを事前に防ぐことができるのです!
ビルド時点でバグの混入を未然に防げるなんてすばらしいですね!
使ってみたくなりませんか?
最後に
Java21には、本日紹介した以外にもいろいろな新機能があります。
個人的には、Generational ZGCを導入することでこれまでのガベージコレクションに比べてどの程度メモリ効率が上がっているのか興味深いですし、VirtualThreadを導入するとどのくらいマルチスレッドベースのアプリケーションの処理速度が増すのか気になるところです。
今後も新機能について調査していけたらと思っております。