Java 21が2023/9/19にリリースされました。
https://mail.openjdk.org/pipermail/jdk-dev/2023-September/008267.html
The Arrival of Java 21!
LTSであり、パターンマッチングや仮想スレッドが正式化され、プレビューとして入ったString Templatesや無名クラス&インスタンスメインメソッドも面白い機能なので、大切なリリースになっていると思います。
詳細はこちら
JDK 21 Release Notes
Java SE 21 Platform JSR 396
OpenJDK JDK 21 GA Release
APIドキュメントはこちら
Overview (Java SE 21 & JDK 21)
追加されたAPIまとめはこちら
https://docs.oracle.com/en/java/javase/21/docs/api/new-list.html
APIの差分はこちら。
https://cr.openjdk.org/~iris/se/21/build/latest/java-se--jdk-20-ga--jdk-21%2B35/
Java 17以降で取り込まれたJEPはこちら
JEPs in JDK 21 integrated since JDK 17
MacやLinuxでのインストールにはSDKMAN!をお勧めします
Oracle OpenJDK以外に無償で商用利用できるディストリビューションとしては、次のようなものがあります。
- Oracle JDK ※対外サーバーの運用には有償ライセンスが必要です
- Adoptium Temurin
- Azul Zulu
- Liberica JDK
- Amazon Corretto 21
- SapMachine
- Microsoft Build of OpenJDK
アップデートは10月に21.0.1が、1月に21.0.2がリリースされることになります。
JEP
大きめの変更はJEPでまとまっています。
https://openjdk.org/projects/jdk/21/
今回は15個のJEPが取り込まれました。すでにプレビューなどで出ていたものが7つあり、そのうち3つが正式化されました。新たに取り込まれたもの8つで、そのうち3つがPreviewです。
長期メンテナンス対象のLTSであり、パターンマッチングや仮想スレッドが正式化されたことでプログラミングに大きな影響がありそうです。またPreviewで入ったString TemplatesやUnnamed Classesも正式化が楽しみな機能です。
Java 21は大切なリリースになっていると思います。
430: String Templates (Preview)
431: Sequenced Collections
439: Generational ZGC
440: Record Patterns
441: Pattern Matching for switch
442: Foreign Function & Memory API (Third Preview)
443: Unnamed Patterns and Variables (Preview)
444: Virtual Threads
445: Unnamed Classes and Instance Main Methods (Preview)
446: Scoped Values (Preview)
448: Vector API (Sixth Incubator)
449: Deprecate the Windows 32-bit x86 Port for Removal
451: Prepare to Disallow the Dynamic Loading of Agents
452: Key Encapsulation Mechanism API
453: Structured Concurrency (Preview)
ツール
ツールに関してのJEPはないのだけど、影響ありそうな変更があります。
importに余分なセミコロンがあるとコンパイルエラーに
文法的に、import部分は「import TypeName ;」のようなimport宣言が複数続くという定義になっているので、importとimportの間にセミコロンが複数続くというのは許されていませんでした。
https://docs.oracle.com/javase/specs/jls/se17/html/jls-7.html#jls-7.5
けれどもjavacでは今までそれを許していたのが、Java 21からはコンパイルエラーになるようになりました。
次のようなコードはエラーになります。
import java.util.List;;;
import java.util.Map;
public class Semi {
}
「余分なセミコロン」というエラーになっています。
% javac Semi.java
Semi.java:1: エラー: 余分なセミコロン
import java.util.List;;;
^
言語仕様が変わったわけではないのですが、いままで通っていたコードがコンパイルエラーになることが有りうるので注意が必要です。
これはコンパイル通ります。
import java.util.List;;;
public class Semi {
}
JShellでツール起動
起動時にTOOLING
をつけることで、JShell内からjavacやjavapなどを呼び出せるようになっています。
javapを呼び出すとおもしろい。
% jshell TOOLING
| JShellへようこそ -- バージョン21
| 概要については、次を入力してください: /help intro
jshell> interface Empty{}
| 次を作成しました: インタフェース Empty
jshell> javap(Empty.class)
Classfile /var/folders/8q/c_36ysxs0pnbwtd667v7yly80000gq/T/TOOLING-16997755864811368347.class
Last modified 2023/09/19; size 191 bytes
SHA-256 checksum 1e53e9d7d4549a00361937701d3b0a613b520a68854310796db7879efc08d195
Compiled from "$JShell$22.java"
public interface REPL.$JShell$22$Empty
minor version: 0
major version: 65
...
#9 = Utf8 REPL/$JShell$22
#10 = Utf8 InnerClasses
#11 = Utf8 Empty
{
}
SourceFile: "$JShell$22.java"
NestHost: class REPL/$JShell$22
InnerClasses:
public static #11= #1 of #8; // Empty=class REPL/$JShell$22$Empty of class REPL/$JShell$22
使えるツールはtools()
で確認できます。
jshell> tools()
jar
javac
javadoc
javap
jdeps
jlink
jmod
jpackage
言語機能
言語機能の変更としては、Record PatternsやPattern Matching for switchが正式機能になりました。これでパターンマッチングの基本的な使い方が一通りできるようになっています。
430: String Templates (Preview)
440: Record Patterns
441: Pattern Matching for switch
443: Unnamed Patterns and Variables (Preview)
445: Unnamed Classes and Instance Main Methods (Preview)
430: String Templates (Preview)
String Templatesは文字列に式の値を埋め込める機能で、文字列補間とかインターポレーションと呼ばれます。
プレビューとして導入されました。Java 22では2nd Previewになりそう。
https://openjdk.org/jeps/459
テンプレートプロセッサ."テンプレート"
の形式になります。テンプレートには"""
で囲む複数行文字列も使えます。テンプレート内の式は\{~}
で書きます。
テンプレートプロセッサには、STR
、FMT
、RAW
が用意されています。STR
は標準でimportされているのでそのまま使えます。
プレビュー機能なので、--enable-preview
が必要です。javac
では--source 21
も必要になります。
% jshell --enable-preview
| JShellへようこそ -- バージョン21
| 概要については、次を入力してください: /help intro
jshell> var a = 123
a ==> 123
jshell> var d = 3.14
d ==> 3.14
jshell> STR."a=\{a} d=\{d}"
$4 ==> "a=123 d=3.14"
変数だけではなく式も扱えます。
jshell> STR."Today is \{new Date()} now"
$1 ==> "Today is Wed Sep 20 05:34:20 JST 2023 now"
FMT
はフォーマットに対応したテンプレートプロセッサです。
jshell> import static java.util.FormatProcessor.FMT
jshell> FMT."a=%05d\{a} d=%.3f\{d}"
$9 ==> "a=00123 d=3.140"
RAW
では文字列に変換する前のデータ構造がそのまま返ります。
jshell> StringTemplate.RAW."a=\{a} d=\{d}"
$11 ==> StringTemplate{ fragments = [ "a=", " d=", "" ], values = [123, 3.14] }
テンプレートプロセッサを自分で作ることもできます。StringTemplate
が渡されるので適切に文字列などのオブジェクトを作る感じです。
jshell> var UPPER = StringTemplate.Processor.of(st -> {
...> var sb = new StringBuilder();
...> for (int i = 0; i < st.values().size(); ++i) {
...> sb.append(st.fragments().get(i));
...> sb.append(st.values().get(i).toString().toUpperCase());
...> }
...> sb.append(st.fragments().getLast());
...> return sb.toString();
...> })
UPPER ==> java.lang.StringTemplate$Processor$$Lambda/0x00000219c1078298@564718df
jshell> var s = "hello"
s ==> "hello"
jshell> var t = "world"
t ==> "world"
jshell> UPPER."message is \{s} and \{t}"
$15 ==> "message is HELLO and WORLD"
RAW
がStringTemplate
オブジェクトを返しているように、結果が文字列である必要はありません。
JEPの説明にあるQueryBuilderの例も面白いので見てみてください。
440: Record Patterns
レコードの分解ができるパターンマッチです。
Java 19でプレビューとして導入されて、Java 21で正式機能になりました。
Java 20には拡張forでのパターンマッチが入ってましたが、Java 21では外されていて別のJEPとしてやりなおすようです。
たとえばPointというレコードを用意します。
jshell> record Point(int x, int y) {}
| 次を作成しました: レコード Point
jshell> var p = new Point(3, 5)
p ==> Point[x=3, y=5]
そうすると次のようなパターンが書けます。
jshell> p instanceof Point(var xx, var yy)
$3 ==> true
条件式と組み合わせて、パターンマッチが成り立つ時にそのまま変数の値を扱うことができます。
jshell> p instanceof Point(var xx, var yy) ?
...> "x:%d y:%d".formatted(xx, yy) :
...> "no match"
$4 ==> "x:3 y:5"
固定値の指定は出来なさそう。
jshell> p instanceof Point(3, var yy)
| エラー:
| 型の開始が不正です
| p instanceof Point(3, var yy)
| ^
Pointを持つBoxを定義します。
jshell> record Box(Point topLeft, Point bottomRight){}
| 次を作成しました: レコード Box
jshell> var b = new Box(new Point(3, 4), new Point(7, 9))
b ==> Box[topLeft=Point[x=3, y=4], bottomRight=Point[x=7, y=9]]
そうすると、Box内のPointの値を直接取り出すことができます。
jshell> b instanceof Box(Point(var left, var top), Point(var right, var bottom)) ?
...> "(%d, %d)-(%d, %d)".formatted(left, top, right, bottom) : "no match"
$8 ==> "(3, 4)-(7, 9)"
次のように命令的にレコードの中身を得ていくよりも、やりたいことがはっきりします。
b instanceof Box b ?
"(%d, %d)-(%d, %d)".formatted(b.topLeft().x(), b.topLeft().y(),
b.bottomRight().x(), b.bottomRight().y()) :
"no match"
441: Pattern Matching for switch
パターンマッチがswitch
で使えるようになります。
Java 17でプレビューとして導入されたものが、Java 21で正式機能になりました。
「といってもパターンマッチってそんなに出番ないのでは?」
と思うかもしれませんが、大きな影響が。
switch
でcase null
が書けるようになります。
jshell> String s = null
s ==> null
jshell> switch (s) {
...> case "test" -> "テスト";
...> case null -> "ぬるぽ";
...> default -> "hello";
...> }
$13 ==> "ぬるぽ"
case null
がない場合は従来どおりNullPointerExceptionです。
jshell> switch (s) {
...> case "test" -> "テスト";
...> default -> "hello";
...> }
| 例外java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "<local0>" is null
| at (#12:1)
""
とnull
で同じ処理を行う場合などに併記できるのが便利です。
jshell> switch(s){
...> case null, "" -> "empty";
...> default -> s;
...> }
$17 ==> "empty"
default
とnull
で同じ処理したいときにアロースタイルで書けないんでは?という疑問に応えて、default
がcase
に書けるようになっています。
jshell> switch (s) {
...> case "one" -> 1;
...> case "two" -> 2;
...> case null, default -> -1;
...> }
$21 ==> -1
で、パターンマッチですが、型と変数をswtichに書けるようになりました。これをタイプパターンといいます。
jshell> Object o = "abc"
o ==> "abc"
jshell> switch (o) {
...> case String s -> "--%s--".formatted(s);
...> case Integer i -> "%03d".formatted(i);
...> default -> o.toString();
...> }
$35 ==> "--abc--"
タイプパターンのあとにwhen
でつなげて条件を書くことでガード節とすることができます。
jshell> o = "helo"
o ==> "helo"
jshell> switch (o) {
...> case String s when s.length() < 5 -> "--%s--".formatted(s);
...> case String s -> s.toUpperCase();
...> case Integer i -> "%05d".formatted(i);
...> default -> o.toString();
...> }
$42 ==> "--helo--"
定数パターンとタイプパターンをひとつのswitchで使うこともできます。定数パターンに使えるのはこれまでどおり整数、文字列、enumです。
jshell> s = "helo"
s ==> "helo"
jshell> switch (s) {
...> case "test" -> "テスト";
...> case String s when s.length() < 5 -> s.toUpperCase();
...> default -> "too long";
...> }
$29 ==> "HELO"
case句のタイプパターンで定義した変数のスコープは、case句の中だけです。
ここではswitch
の外にあるのと同じく変数s
を定義してcase句で上書きしていますが、switchの外に影響はありません。
jshell> s = "helo"
s ==> "helo"
jshell> switch (s) {
...> case String s when s.length() < 5 -> (s="Hello").toUpperCase();
...> default -> "too long";
...> }
$30 ==> "HELLO"
jshell> s
s ==> "helo"
Record Patternsと併用が本命ですね。
return switch(n) {
case IntExpr(int i) -> i;
case NegExpr(Expr n) -> -eval(n);
case AddExpr(Expr left, Expr right) -> eval(left) + eval(right);
case MulExpr(Expr left, Expr right) -> eval(left) * eval(right);
default -> throw new IllegalArgumentException(n);
};
ここでSealed Classと組み合わせて次のような定義になっているとします。
sealed interface Expression
permits IntExpr, NegExpr, AddExpr, MulExpr {}
record IntExpr(int i) implements Expression {}
record NegExpr(Expr n) implements Expression {}
record AddExpr(Expr left, Expr right) implements Expression {}
record MulExpr(Expr left, Expr right) implements Expression {}
そうすると、Expressionが取りうるのが4つのレコードで全てということがわかるのでdefault句が不要になります。また、全てをチェックしていることを保証できるのが、既存のコードと違うところで、パターンマッチのメリットです。
Expression n = getExp();
return switch(n) {
case IntExpr(int i) -> i;
case NegExpr(Expr n) -> -eval(n);
case AddExpr(Expr left, Expr right) -> eval(left) + eval(right);
case MulExpr(Expr left, Expr right) -> eval(left) * eval(right);
};
443: Unnamed Patterns and Variables (Preview)
パターンマッチやラムダ引数、catch句など、構文的に変数を指定しないといけないけどその変数を使わないということがよくあります。
_
を使うことで、値を使わないことを明示できます。
プレビューですが、ドラフトとしてプレビューなしのJEPがあがってきているので、Java 22で正式化しそうです。
Java 21では--enable-preview
が必要です。javac
では--source 21
も必要になります。
無名パターン変数
例えば上記のAddExpr
に対するパターンでleft
しか使わない次のような場合を考えます。
case AddExpr(Expr left, Expr right) -> print(left);
このときright
は使っていません。そこで次のように_
を使って不要な値であることを明示できます。
case AddExpr(Expr left, Expr _) -> print(left);
ここで、型にもこだわらないのであればvar
が使えます。
case AddExpr(Expr left, var _) -> print(left);
そしてこの場合は、var
を省略できます。
case AddExpr(Expr left, _) -> print(left);
仕様的には、これは「無名パターン」となっています。
無名変数
パターンマッチだけではなく変数でも_
が使えます。
使えるのは次の6か所。
- ローカル変数
- try-with-resource
- for文
- 拡張for文
- catch句
- ラムダ式の引数
一番出番がありそうなところはcatch句になりそうです。例外を捕まえたいけど値は使わないということ多いですね。
try {
Thread.sleep(500);
} catch (InterruptedException _) {}
処理もなにもしないのに変数名をe
にするかex
にするか悩むのもばからしいですね。
ラムダ式でも引数を使わないことがよくあります。
var button = new JButton("押す");
button.addActionListener(_ -> System.out.println("押された"));
ただ、オーバーライドしたメソッドの引数を使わないということも多いですが、引数には使えないようです。
445: Unnamed Classes and Instance Main Methods (Preview)
パブリックスタティックヴォイドメインの呪文から解放されるやつです。
Javaがパブリックスタティックヴォイドメインの呪文から解放される - きしだのHatena
Javaでは単純なハローワールドを書くために次のようなコードが必要でした。
public class Hello {
public static void main(String[] args) {
System.out.println("Hello Java!");
}
}
これが次のように書けるようになります。
void main() {
System.out.println("Hello Java!");
}
プレビュー機能なので、--enable-preview
が必要です。javac
では--source 21
も必要になります。ソースを直接実行する場合はjava
コマンドでも--source 21
が必要です。
>more Hello.java
void main() {
System.out.println("Hello Java!");
}
>java --enable-preview --source 21 Hello.java
ノート: Hello.javaはJava SE 21のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Hello Java!
public
やclass
、static
などのキーワードが消え、[]
という謎の記号も消えました。
プログラムを勉強するときに、まずやりたいことは処理を書くことです。
クラスは書いた処理をうまく構成するための仕組みなので、処理が書けないうちに勉強してもあまり意味がありません。
public
などアクセス指定はプログラムが大きくなったときに不適切な要素を使ってしまわないための仕組みなので、入門時のサンプルでは不要です。
static
を説明するにはクラスやインスタンスの理解が必要になりますが、処理が書ける前に勉強するには早すぎます。
配列も変数を知らないうちに勉強できるものでもなく、入門時のサンプルで引数args
を使うことはあまりありません。
その結果「よくわからないしきたり」のまま放置されがち、というか放置せざるを得ない状態で「System.out.pritlnというのは~」という説明をすることになりますが、クラス名とファイル名が違うので動かせなくてハマってそこまでたどりつけなかったりもします。
ということで、初期に学習するべきことに集中できるようにするために、次のように制約が緩和されました。
- クラスの定義が不要になる
- mainメソッドはインスタンスメソッドでよくなる
- mainメソッドの引数を省略できる
- mainメソッドがpublicじゃなくてもよくなる
「メソッドも不要でいいのでは?」となると思いますが、現状ではステートメントとメソッドを同レベルで書く仕組みがないため、新たにローカルメソッドのような仕組みが必要になり、「初期に学習するべきことに集中できるようにするため」としては影響範囲が大きいので残されています。
System.out
も邪魔ではーという点は、「System.printlnメソッド定義してデフォルトでstatic importすればいいよね」ということを言ってるので、そのうち入ると思います。
このあたりは、次のデザインノートにまとめられています。
https://openjdk.org/projects/amber/design-notes/on-ramp
クラスの定義が不要
クラスを知らなくていいことの他に、クラス名を考えなくていいとかインデントが一段浅くなるとか、中カッコが一組だけになるので間違いが減るとか、いろいろ入門がやりやすくなります。
クラスを省略すると、new Object(){}
で囲まれることになりました。
new Object() {
void main() {
System.out.println("Hello Java");
}
}.main();
mainメソッドはインスタンスメソッドでよくなる
mainメソッドにstaticをつけなくてよくなります。そして、mainメソッドにstaticをつけなくていいということは、そこから呼び出すメソッドなどにもstaticをつけなくていいということになるので、少し大きめのサンプルが書きやすくもなります。
public class Hello {
public void main(String[] args) {
foo();
}
void foo() {
System.out.println("Hello");
}
}
mainメソッドの引数を省略できる / mainメソッドがpublicじゃなくてもよくなる
書かなくてよさそうなものを書かずにすむのですっきりします。
mainメソッドをprivateにすることはできません。protectedは可能です。
「public static void main(String[] args)」を何も見ずに書けるようになったときにJavaに馴染んだ満足感があったので、それがなくなるのは寂しいですが、単なるノスタルジーなのでなくていいと思います。
API
APIの変更としては7つのJEPがありますが、JEPになっていない変更も結構あります。
431: Sequenced Collections
442: Foreign Function & Memory API (Third Preview)
444: Virtual Threads
446: Scoped Values (Preview)
448: Vector API (Sixth Incubator)
452: Key Encapsulation Mechanism API
453: Structured Concurrency (Preview)
小さいもの
JEPになっていないAPI変更で、動きがわかりやすいものを挙げます。
数値が指定範囲に収まるようにする
数値が指定範囲に収まるようにするclamp
メソッドがMath
クラスに用意されています。
型違いで次の4つが用意されています。
int Math.clamp(long value, int min, int max)
long Math.clamp(long value, long min, long max)
double Math.clamp(double value, double min, double max)
float Math.clamp(float value, float min, float max)
たとえば、数値を10~100に制限するならこんな感じ。
jshell> Math.clamp(1234, 10, 100)
$13 ==> 100
jshell> Math.clamp(1, 10, 100)
$14 ==> 10
int
を返すclamp
メソッドの最初の引数はlong
が受け取れることにも注意が必要です。long
をint
の範囲におさめることができます。
jshell> Math.clamp(Long.MAX_VALUE, 10, 100)
$15 ==> 100
jshell> /var $15
| int $15 = 100
long
を返したい場合には、第二引数か第三引数をlong
にする必要があります。
jshell> Math.clamp(Long.MAX_VALUE, 10L, 100)
$16 ==> 100
jshell> /var $16
| long $16 = 100
区切り文字を含む文字列分割
文字列分割ではsplit
メソッドがありますが、ここには分割文字列は含まれません。正規表現を指定した場合には、どういう文字列で分割したか知りたいこともあるので、分割文字列も含まれるsplitWithDelimiter
メソッドがString
とPattern
に導入されました。
jshell> "aa:bbb::cc:::dd".split(":+")
$22 ==> String[4] { "aa", "bbb", "cc", "dd" }
jshell> "aa:bbb::cc:::dd".splitWithDelimiters(":+", 0)
$23 ==> String[7] { "aa", ":", "bbb", "::", "cc", ":::", "dd" }
2番目の引数は、何分割するかの制限です。n分割にすると2n-1要素が返ってきます。
jshell> "aa:bbb::cc:::".splitWithDelimiters(":+", 4)
$24 ==> String[7] { "aa", ":", "bbb", "::", "cc", ":::", "" }
jshell> "aa:bbb::cc:::".splitWithDelimiters(":+", 3)
$25 ==> String[5] { "aa", ":", "bbb", "::", "cc:::" }
0や負の数の場合は可能な限り分割されますが、分割文字列で終わる場合に0の場合は最後に空文字列は含まず、負の場合は空文字列が含まれます。
jshell> "aa:bbb::cc:::".splitWithDelimiters(":+", 0)
$26 ==> String[6] { "aa", ":", "bbb", "::", "cc", ":::" }
jshell> "aa:bbb::cc:::".splitWithDelimiters(":+", -1)
$27 ==> String[7] { "aa", ":", "bbb", "::", "cc", ":::", "" }
範囲指定のindexOf
文字列中の文字・文字列検索で範囲が指定できるようになります。
これまで、検索開始位置は指定できていましたが、終了位置も指定できるようになっています。
jshell> "test".indexOf('t', 1, 2)
$20 ==> -1
jshell> "test".indexOf('t', 1)
$21 ==> 3
isEmoji
絵文字判定がCharacter
クラスに取り込まれています。
jshell> Character.isEmoji('A')
$1 ==> false
jshell> Character.isEmoji('⚽')
$2 ==> true
ただ多くの絵文字では次のようになります。
jshell> Character.isEmoji('🤗')
$3 ==> false
jshell> Character.isEmoji('🏀')
$4 ==> false
多くの絵文字がサロゲートペアになっていて、char
型の値がふたつで1つの絵文字を表すからですね。
jshell> Integer.toHexString("⚽".codePointAt(0))
$5 ==> "26bd"
jshell> Integer.toHexString("🏀".codePointAt(0))
$6 ==> "1f3c0"
jshell> Integer.toHexString("🤗".codePointAt(0))
$7 ==> "1f917"
なので、char
単体で判定するのではなく、文字列からcodePointAt
メソッドでコードを取得して判定する必要があります。
jshell> Character.isEmoji("🤗".codePointAt(0))
$8 ==> true
他にも、絵文字関連で全部で6つのメソッドが用意されています。
jshell> Character.isE
isEmoji( isEmojiComponent( isEmojiModifier( isEmojiModifierBase(
isEmojiPresentation( isExtendedPictographic(
StringBuilder/StringBufferのrepeatメソッド
jshell> var s = new StringBuilder("test")
s ==> test
jshell> s.repeat("er", 3)
$42 ==> testererer
Formatterでの浮動小数点数の表示が微妙に違う
Java 20
jshell> "%.16e".formatted(2e23)
$1 ==> "1.9999999999999998e+23"
Java 21
jshell> "%.16e".formatted(2e23)
$1 ==> "2.0000000000000000e+23"
これはDouble.toString
がJava 19で変わったものに合わせられたようです。
jshell> Double.toString(2e23)
$2 ==> "2.0E23"
Java 18では次のようになっていました。
jshell> Double.toString(2e23)
$1 ==> "1.9999999999999998E23"
HttpClientがAutoClosableに
HttpClientにcloseメソッドなどが追加されて、try-with-resourceで指定できるようになりました。
try (var client = HttpClient.newHttpClient()) {
var req = HttpRequest.newBuilder(
URI.create("http://example.com")).build();
var res = client.send(req, BodyHandlers.ofString());
System.out.println(res.body());
}
他に追加されたメソッドは次のようなものです。
void shutdown()
void shutdownNow()
boolean awaitTermination(Duration duration)
boolean isTerminated()
431: Sequenced Collections
List
など順序付きのコレクションに共通のインタフェースが導入されます。
interface SequencedCollection<E> extends Collection<E> {
// new method
SequencedCollection<E> reversed();
// methods promoted from Deque
void addFirst(E);
void addLast(E);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
getLast
便利。
あと、reversed
もたまにありがたいですね。
jshell> List.of(1, 2, 3).reversed()
$3 ==> [3, 2, 1]
442: Foreign Function & Memory API (Third Preview)
3rd Previewになりました。JEP 454としてPreviewなしのJEPが控えているので、Java 22で正式化されそうです。
コンパイル、実行では--enable-preview
を付ける必要があります。
Foreign Memory API
ヒープ外のメモリをアクセスする方法としては、ByteBufferを使う方法やUnsafeを使う方法、JNIを使う方法がありますが、それぞれ一長一短があります。
ByteBufferでdirect bufferを使う場合、intで扱える範囲の2GBまでに制限されたり、メモリの解放がGCに依存したりします。
Unsafeの場合は、性能もいいのですが、名前が示すとおり安全ではなく、解放済みのメモリにアクセスすればJVMがクラッシュします。
JNIを使うとCコードを書く必要があり、性能もよくないです。
ということで、ヒープ外のメモリを直接扱うAPIがJava 14でインキュベータモジュールとして導入され、今回8バージョン目です。
次のようなコードになります。
import java.lang.foreign.*;
try (Arena session = Arena.ofAuto()) {
MemorySegment seg = session.allocate(100)
for (int i = 0 ; i < 25 ; i++) {
seg.set(ValueLayout.JAVA_INT, i, i);
}
}
Foreign Function API
ネイティブライブラリの呼び出しを行う。
外部メモリのアクセスにはForeign Memory Access APIを使う
JNIの代替
Java 16でForeign Linkerとして1stインキュベータになり、今回6バージョン目のインキュベータです。
たとえばこんな感じのC関数があって。
size_t strlen(const char *s);
こんな感じでMethodHandle
を取り出して。
Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").get(),
FunctionDescriptor.of(JAVA_LONG, ADDRESS)
);
こんな感じで呼び出すようです。
MemorySegment cString = arena.allocateUtf8String("Hello");
long len = strlen.invoke(cString); // 5
444: Virtual Threads
仮想スレッドが正式化されました。
Java 20からの違いは、ThreadLocal
が仮想スレッドでも使えるようになったことです。
いままでJavaは、初期にプラットフォームスレッドを使うようになって以来、ずっとThreadとしてプラットフォームスレッドを使ってきました。
プラットフォームスレッドというのは、OSが管理するようなスレッドです。OSが管理するスレッドというのは、YouTubeで動画を見てTwitterして時計を表示してファイナルファンタジーやって、と何でも同時に動かせるように設計されています。
もちろんアプリケーションの中でいろいろな働きのスレッドを動かすのですが、多くのスレッドを立ち上げて行うことになるのは結局同じ処理です。
その中で、計算のために同じことをたくさん並列に動かすという場合にはハードウェアの構成を考えないといけなくて、そのための構成がGPUだったりVector APIで扱うSIMD命令だったりするわけですね。
そして、アプリケーションの処理としては結局のところ計算の時間よりも通信待ちの時間のほうがはるかに長いということで、通信待ち時間を有効利用したいというのが並列処理の主な目的になっていました。
そうすると、計算の最中にバランスとりながら処理を切り替えるプラットフォームスレッドというのはオーバークオリティだったわけで、やりたいことに対してメモリも食うし限界も低いし遅いということになってたわけです。
ということで、現状でJavaではReactiveやRxといって通信のときに他の処理に制御を渡せるようなコードを書いているのですが、これが非常に難しくてやってられないということで、Javaから離れてKotlinのように自然にそういう処理が書ける言語に移行する人がでています。
という問題に対処するために、OSではなくJVMが管理するスレッドがVirtual Threadとして導入されます。Virtual ThreadはProject Loomで開発されていました。
Virtual Threadは既存のプラットフォームスレッドと同様に扱うことができるよう気をつけてAPIが設計されています。
ただ、プラットフォームスレッドではnew Thread()
としてスレッドを生成できましたが、コンストラクタでVirtual Threadを生成することはできません。
いままでのプラットフォームスレッドはこんな感じで使いました。
jshell> Thread t = new Thread(() -> System.out.println("hello"))
t ==> Thread[#220496,Thread-110304,5,main]
jshell> t.getState()
$33 ==> NEW
jshell> t.start()
jshell> hello
jshell> t.getState()
$35 ==> TERMINATED
Thread.Builderが用意されて、ofPlatform
かofVirtual
でプラットフォームスレッドと仮想スレッドを分けれるようになります。
unstarted
メソッドを使うと、new Thread
と同様にまだ開始していないスレッドを得れます。
jshell> Thread t = Thread.ofPlatform().unstarted(() -> System.out.println("hello"))
t ==> Thread[#220491,Thread-110303,5,main]
jshell> t.start()
hello
jshell> t.getState()
$24 ==> TERMINATED
そしてofVirtual
に変更すると仮想スレッドを得れます。仮想スレッドはThreadクラスのオブジェクトとして扱えます。使い方はプラットフォームスレッドと同様です。
jshell> Thread t = Thread.ofVirtual().unstarted(() -> System.out.println("hello"))
t ==> VirtualThread[#220492]/new
jshell> t.start()
jshell> hello
jshell> t.getState()
$27 ==> TERMINATED
start
メソッドを使うと、スレッドの実行とThread
オブジェクトの取得が同時に行えます。
jshell> Thread t = Thread.ofVirtual().start(() -> System.out.println("hello"))
t ==> VirtualThread[#220497]/runnable
hello
jshell> t.getState()
$37 ==> TERMINATED
ExecutorService
で仮想スレッドを使う場合は、Executors.newVirtualThreadPerTaskExecutor
を使います。
jshell> ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()
ex ==> java.util.concurrent.ThreadPerTaskExecutor@28ba21f3
jshell> ex.submit(() -> System.out.println("hello"))
$40 ==> java.util.concurrent.ThreadPerTaskExecutor$ThreadBoundFuture@64a294a6[Not completed]
hello
jshell> ex.isTerminated()
$41 ==> false
jshell> ex.isShutdown()
$42 ==> false
jshell> ex.shutdown()
jshell> ex.isTerminated()
$44 ==> true
jshell> ex.isShutdown()
$45 ==> true
では、少し性能を見てみましょう。
3秒待つメソッドを定義しておきます。
jshell> import java.time.Duration
jshell> void s(){ try{ Thread.sleep(Duration.ofSeconds(3));} catch(Exception _){}}
| 次を作成しました: メソッド s()
プラットフォームスレッドを100起動してそれぞれで3秒待ちます。
jshell> IntStream.range(0, 100).forEach(_ -> Thread.ofPlatform().start(() -> s()))
スレッドが100増えてすぐ元にもどっていることがわかります。
仮想スレッドを100起動してそれぞれで3秒待ちます。
jshell> IntStream.range(0, 100).forEach(_ -> Thread.ofVirtual().start(() -> s()))
そうすると、17スレッドだけ増えて31スレッドになります。
100増えないことと、終わってもスレッドが残っていることがわかります。
それでは10万スレッドを同時に実行してみましょう。
まずは仮想スレッドから。
jshell> IntStream.range(0, 100_000).forEach(_ -> Thread.ofVirtual().start(() -> s()))
スレッド数は33のまま、CPU使用率は1%くらいです。メモリは120MBくらい使っていますね。
プラットフォームスレッドで試してみます。
jshell> IntStream.range(0, 100_000).forEach(_ -> Thread.ofPlatform().start(() -> s()))
まず、しばらく処理が終わりません。
スレッドは1万5000くらいまで使われていますが、その1万5000のスレッドでスレッドが終わったら別のスレッドというふうにやりくりして10万スレッドを処理できるまで、繰り返されます。
また、CPUは80%近くまで使われています。処理はSleepするだけなので、スレッド切り替えにCPUを使っていると推測されます。メモリも600MBまで使っていますね。
このように、多数のスレッドを処理するときにプラットフォームスレッドでは同時処理数に限界があることと、CPUパワーを余分に使ってしまうので、軽量な仮想スレッドが求められ実装されたわけです。
446: Scoped Values (Preview)
IncuvatorからPreviewになりました。機能的な違いは無いようです。
同じスレッド内で値を共有したいときThreadLocal
を使いますが、値の変更が可能であったり子スレッドに値が引き継がれたり少し重いので、より限定された仕組みを提供する、ということのようです。
つまり、値を引数でひっぱりまわすのは面倒なのでグローバル変数的にstaticフィールドを使いたい程度のモチベーションで値を共有化するときに、スレッドセーフのためのThreadLocalは重すぎる、という感じですね。
たとえば次のような処理があります。
void start() {
proc1("test");
}
void proc1(String str) {
System.out.println(str);
proc2(str);
}
void proc2(String str) {
System.out.println(str);
}
これを、全部のメソッドにいちいち引数を設定して値をひきまわるのは面倒なのでフィールドを使おう、という場合。
String str;
void start() {
str = "test";
proc1();
}
void proc1() {
System.out.println(str);
proc2();
}
void proc2() {
System.out.println(str);
}
これは複数スレッドから呼び出されると正しく動かないことがあります。
スレッドセーフにするためにThreadLocal
を使っていました。
final ThreadLocal<String> VAR = new ThreadLocal<>();
void start() {
VAR.set("test");
proc1();
}
void proc1() {
System.out.println(VAR.get());
proc2();
}
void proc2() {
System.out.println(VAR.get());
}
しかし、引数を書いて値を持ちまわっていくのめんどいね、くらいのモチベーションで使うにはThreadLocal
は重過ぎるので、軽量な値共有手段としてScopedValue
が導入されます。
final ScopedValue<String> VAR = new ScopedValue<>();
void start() {
ScopedValue.where(VAR, "test")
.run(() -> proc1());
}
void proc1() {
System.out.println(VAR.get());
proc2();
}
void proc2() {
System.out.println(VAR.get());
}
448: Vector API (Sixth Incubator)
6th Incubatorになりました。
AVX命令のような、複数のデータに対する計算を同時に行う命令をJavaから利用できるようになります。
Project Panamaのひとつとして開発されていました。
Java 16でインキュベータとして導入されましたが、今回6thインキュベータになりました。
使うためには実行時やコンパイル時に--add-modules jdk.incubator.vector
をつける必要があります。
import jdk.incubator.vector.*;
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_256;
void vectorComputation(float[] a, float[] b, float[] c) {
for (int i = 0; i < a.length; i += SPECIES.length()) { // SPECIES.length() = 256bit / 32bit -> 8
VectorMask<Float> m = SPECIES.indexInRange(i, a.length); // 端数がマスクされる
// a.lengthが11でiが8のとき最初の3つしか要素がないので [TTT.....]
// FloatVector va, vb, vc;
FloatVector va = FloatVector.fromArray(SPECIES, a, i, m);
FloatVector vb = FloatVector.fromArray(SPECIES, b, i, m);
FloatVector vc = va.mul(va).
add(vb.mul(vb)).
neg();
vc.intoArray(c, i, m);
}
}
利用できるのは次の6つの型です。それぞれに対応するVector型があって、これが基本になります。
型 | bit幅 | Vector |
---|---|---|
byte | 8 | ByteVector |
short | 16 | ShortVector |
int | 32 | IntVector |
long | 64 | LongVector |
float | 32 | FloatVector |
double | 64 | DoubleVector |
ただ、利用するにはVectorSpeciesが必要です。利用したいVectorにSPECIES_*という定数が用意されているので、それを使います。*は一度に計算するbit数ですね。
jshell> FloatVector.SP
SPECIES_128 SPECIES_256 SPECIES_512
SPECIES_64 SPECIES_MAX SPECIES_PREFERRED
MAXではそのハードウェアで使える最大、PREFERREDは推奨ビット数だけど、同じになるんじゃないのかな。ここでは256bitが推奨されて、floatが8個同時に計算できるようになっていますね。
jshell> FloatVector.SPECIES_PREFERRED
$11 ==> Species[float, 8, S_256_BIT]
ハードウェアで使えるbit数は搭載CPUに依存しますが、普通のIntel/AMDであれば256、XEONとか つよつよCPUなら512かな。M1は128でした。ハードウェアでサポートされないbit数を使おうとするとソフトウェア処理になるので遅くなります。
実際のVectorはfrom*というメソッドで取得します。fromArray、fromByteArray、fromByteBufferが用意されています。インキュベータに入る前はfromValuesがあったのですが、なくなってますね。
Vectorを得られたら、用意されたメソッドで計算します。ひととおりの算術命令はあります。
jshell> va.
abs() add( addIndex(
bitSize() blend( broadcast(
byteSize() castShape( check(
compare( compress( convert(
convertShape( div( elementSize()
elementType() eq( equals(
expand( fma( getClass()
hashCode() intoArray( intoMemorySegment(
lane( lanewise( length()
lt( maskAll( max(
min( mul( neg()
notify() notifyAll() pow(
rearrange( reduceLanes( reduceLanesToLong(
reinterpretAsBytes() reinterpretAsDoubles() reinterpretAsFloats()
reinterpretAsInts() reinterpretAsLongs() reinterpretAsShorts()
reinterpretShape( selectFrom( shape()
slice( species() sqrt()
sub( test( toArray()
toDoubleArray() toIntArray() toLongArray()
toShuffle() toString() unslice(
viewAsFloatingLanes() viewAsIntegralLanes() wait(
withLane(
ところで、こういったメソッド呼び出しの内部でAVX命令などを呼び出すのでは遅くなるんではという気がしますが、実際にはJVM intrinsicsという仕組みでJITコンパイラがこれらのメソッド呼び出しをネイティブ関数呼び出しに置き換えます。
452: Key Encapsulation Mechanism API
暗号化の方式で公開鍵暗号というものがあります。秘密鍵と公開鍵を用意して、公開鍵を人に教えておくと、その公開鍵で暗号化されたメッセージは秘密鍵だけで復号できるというものです。
ただ、だいたい公開鍵暗号は遅いので、通信時には暗号化にも復号にも同じ鍵を使う共通鍵暗号を使うということが行われます。そのときの共通鍵をランダムに生成して公開鍵暗号で送るわけですね。
そういった、公開鍵暗号を使って共通鍵暗号を送るときにいろんな方式があるようなんですかが、そのためのAPIが用意されたということです。
453: Structured Concurrency (Preview)
Second IncubatorからPreviewになりました。
fork
メソッドはSubtask
を返すようになっています。
並列処理では、複数の処理を実行するときに、両方が終われば正常終了とか、どちらか片方が終われば終了だとか、どちらか一方でも例外が発生したら終了だとか、同時に行う処理で連動することがあります。
しかし、これを既存のjoin
やwait
などで制御しようとすると、実際にはjoin
からwait
へのGo Toを書くようなコードになって、処理が追えなくなります。
そこで導入されるのが構造化並列性といいます。
こんな感じ。詳しくはあとで書きます!(Java 19のときから言ってる・・・)
Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<String> user = scope.fork(() -> findUser());
Subtask<Integer> order = scope.fork(() -> fetchOrder());
scope.join() // Join both forks
.throwIfFailed(); // ... and propagate errors
// Here, both forks have succeeded, so compose their results
return new Response(user.get(), order.get());
}
}
Subtask
はSupplier
を継承しているので、Supplier
として扱うほうがいいかもしれません。
JVM
JVMの変更として2のJEPが導入されています。
439: Generational ZGC
451: Prepare to Disallow the Dynamic Loading of Agents
439: Generational ZGC
ZGCに世代別GCが導入されて、よりパフォーマンスがよくなりました。有効にするためには、++UseZGC
の他に+ZGenerational
を付ける必要があります。
$ java -XX:+UseZGC -XX:+ZGenerational
451: Prepare to Disallow the Dynamic Loading of Agents
Javaエージェントは、クラス読み込み時に処理を行って、クラスを加工するための仕組みです。プロファイリングなどに使われます。
そのエージェントの動的ロードを将来的に禁止するため、動的に読み込もうとすると警告が出るようになります。
JDK
449: Deprecate the Windows 32-bit x86 Port for Removal
JDKの変更、つまりリリース体系などの変更としては、Windows用のx86 32bitが非推奨になって、そのうち削除されるようです。