LoginSignup
128
83

Java 21新機能まとめ

Last updated at Posted at 2023-09-21

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以外に無償で商用利用できるディストリビューションとしては、次のようなものがあります。

アップデートは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

テンプレートプロセッサ."テンプレート"の形式になります。テンプレートには"""で囲む複数行文字列も使えます。テンプレート内の式は\{~}で書きます。
テンプレートプロセッサには、STRFMTRAWが用意されています。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"

RAWStringTemplateオブジェクトを返しているように、結果が文字列である必要はありません。
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で正式機能になりました。

「といってもパターンマッチってそんなに出番ないのでは?」
と思うかもしれませんが、大きな影響が。

switchcase 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"

defaultnullで同じ処理したいときにアロースタイルで書けないんでは?という疑問に応えて、defaultcaseに書けるようになっています。

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!

publicclassstaticなどのキーワードが消え、[]という謎の記号も消えました。
プログラムを勉強するときに、まずやりたいことは処理を書くことです。
クラスは書いた処理をうまく構成するための仕組みなので、処理が書けないうちに勉強してもあまり意味がありません。
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が受け取れることにも注意が必要です。longintの範囲におさめることができます。

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メソッドがStringPatternに導入されました。

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が用意されて、ofPlatformofVirtualでプラットフォームスレッドと仮想スレッドを分けれるようになります。
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増えてすぐ元にもどっていることがわかります。
image.png

仮想スレッドを100起動してそれぞれで3秒待ちます。

jshell> IntStream.range(0, 100).forEach(_ -> Thread.ofVirtual().start(() -> s()))

そうすると、17スレッドだけ増えて31スレッドになります。

image.png

100増えないことと、終わってもスレッドが残っていることがわかります。

それでは10万スレッドを同時に実行してみましょう。
まずは仮想スレッドから。

jshell> IntStream.range(0, 100_000).forEach(_ -> Thread.ofVirtual().start(() -> s()))

スレッド数は33のまま、CPU使用率は1%くらいです。メモリは120MBくらい使っていますね。

image.png

プラットフォームスレッドで試してみます。

jshell> IntStream.range(0, 100_000).forEach(_ -> Thread.ofPlatform().start(() -> s()))

まず、しばらく処理が終わりません。
スレッドは1万5000くらいまで使われていますが、その1万5000のスレッドでスレッドが終わったら別のスレッドというふうにやりくりして10万スレッドを処理できるまで、繰り返されます。
また、CPUは80%近くまで使われています。処理はSleepするだけなので、スレッド切り替えにCPUを使っていると推測されます。メモリも600MBまで使っていますね。

image.png

このように、多数のスレッドを処理するときにプラットフォームスレッドでは同時処理数に限界があることと、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を返すようになっています。

並列処理では、複数の処理を実行するときに、両方が終われば正常終了とか、どちらか片方が終われば終了だとか、どちらか一方でも例外が発生したら終了だとか、同時に行う処理で連動することがあります。
しかし、これを既存のjoinwaitなどで制御しようとすると、実際には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());
    }
}

SubtaskSupplierを継承しているので、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が非推奨になって、そのうち削除されるようです。

128
83
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
128
83