2024/11/10

で、ValhallaのValue Classってどうなったの? その1
(JJUG CCC 2024 Fall)

このエントリーをはてなブックマークに追加

10月27日にJJUG CCC 2024 Fallが開催されました。

久しぶりのベルサール新宿グランド。前回までの野村コンファレンスプラザ新宿に比べると、部屋も増えて、参加者も大幅に増えたようです。

で、さくらばはProject Valhallaで策定されているValue Classについてプレゼンしてきました。資料はこちら。

なお、Value Classはまだ策定中の仕様なので、今後変化する可能性があります。本エントリーで説明していることも変わる可能性が高いので、その点はご了承ください。

Value Classを説明する前に、そもそもProject Valhallaとは何なのか?

Project ValhallaはJavaの型システムを見直して、整理するためのOpenJDKのサブプロジェクトです。

Valhallaとは北欧神話に出てくる主神オーディンの宮殿のことです。戦士の魂が最終的に集められるのがValhallaで、日本人的な感覚だとあまり縁起のいい場所ではないような気がするんですけど、どうなんでしょう。

ちなみに、上の資料の表紙の背景にある道は、スゥエーデンのストックホルムにあるヴァルハラ通りです。今年の2月にストックホルムで開催されたJfokusに参加したので、ついでにValhallaにも行ってみたわけですw

そんなこんなで、資料の背景の写真はすべてストックホルムで撮った写真を使ってます。

 

さて、Project Valhallaです。

Valhallaでは型の再整理を行っているのですが、主な論点としては以下の4つがあります。

Value ClassがValhallaのメインとなる論点で、この後説明していきます。

Value Classの導入過程で必要となったのが、Null-Restricted/Nullable Typeです。

Specialized Genericsというのは、ジェネリクスの型パラメータにプリミティブ型も使用できるようにしようというものです。

Primitive拡張とSpecialized Genericsは、Value ClassとNull-Restricted/Nullable Typeに比べると、仕様策定にまだまだ時間がかかりそうなので、ここでは触れません。

 

Value Class導入の背景

Value Classの解説をする前に、まずValue Classの導入の背景について説明しましょう。

ご存じの通り、Javaには2種類の型があります。一方がプリミティブ型、もう一方が参照型です。

クラスでオブジェクトを作ってというのは、すべて参照型ですね。また、Javaでは配列も参照型となります。

Javaの言語仕様的にはプリミティブ型と参照型には以下のような違いがあります。

最後の初期化はプリミティブ型ではデフォルト値(たとえば数値型であれば0)があり、初期化しなくてもデフォルト値で使用できます。一方の参照型では必ずnewをしてオブジェクトを生成して初期化しなくてはならないということです。参照型変数のデフォルト値としてnullがありますが、これは変数のデフォルト値であってオブジェクトのデフォルト値ではないです。

言語仕様的なこのような違いはありますが、Valhallaで着目しているのは2つの型がヒープでどのように扱われているかということです。

たとえば、doubleの配列を考えてみます。

配列も参照型のオブジェクトなので、ヒープに生成する場合、オブジェクトヘッダーとフィールド用の領域を確保します。

プリミティブ型の値を配列に格納する場合、フィールド用の領域に直接値が書きこまれます。

一方、参照型の配列の場合、フィールド用の領域には要素のオブジェクトへの参照が格納されます(これが参照型と呼ばれる理由です)。

たとえば、2つのdoubleの要素を持つPointレコードの場合を示したのが、以下の図です。

Pointオブジェクトがヒープ上のどこに配置されるのかについては、JVMまかせでユーザーは指定できません。このため、Pointオブジェクトが離れた位置に配置されることもあります。

 

CPUのメモリアクセス

ここで、CPUがどのようにメモリにアクセスするかを紹介しておきましょう。

CPUの内部ではALU (Arithmetic Logic Unit、演算ユニット)が演算を行うのですが、そのためのデータはレジスターに格納します。

レジスターは高速ですが小容量なので、他のデータはメインメモリーに配置されます。必要に応じて、メインメモリーからレジスターにデータをロードします(もちろん、その逆方向もあります)。

しかし、メインメモリーは速度が遅いため、メインメモリーに直接アクセスするとCPUがアイドル状態になってしまいます。このため、現代のCPUではレジスターとメインメモリーの間にはキャッシュを配置しています。

その構成を表したのが以下の図です。

キャッシュはL1, L2, L3の3レベルあり、数字が少ないほど高速ですが、容量は少なくなります。

L1にデータがあれば数クロックでアクセスできますが、キャッシュにないデータをロードする場合桁違いに遅くなるわけです。

これをキャッシュミスと呼びます。

データにアクセスする時に、なるべくキャッシュミスが起こらないようにするのが、パフォーマンスを向上させる秘訣になります。

メインメモリーからキャッシュにデータをロードする時は、1つ1つのデータではなく、ある程度まとまった単位(チャンク)でロードします。これは、あるデータを使用する時、その近くにあるデータにもアクセスする傾向があるからです。たとえば、ループで配列をイテレートする場合などですね。

 

参照型配列のヒープ使用効率

前節で一緒に使うデータをなるべく近くに配置すれば、キャッシュミスが発生しないことを説明します。ところが、参照型の配列だとどうでしょう。

Pointオブジェクトはヒープ上で固まって配置されるとは保証されません。つまり、キャッシュミスを引き起こす可能性が高いということです。

デフォルトで使用されるG1GCの場合、メモリを領域で区分し、Young領域とOld領域に分けられます。そして、新しいオブジェクトは基本的にYoung領域に配置されます。

このため、上記のPointオブジェクトがヒープ上で遠く離れた位置に作られる可能性は少ないのですが、複数のYoung領域に分かれて生成させることはあるかもしれません。このような場合に、キャッシュミスを引き起こしてしまうわけです。

では、どうすればよいでしょう?

プリミティブ型の値と同じようにデータを直接フィールド領域に格納してしまえばいいということです。つまり、下図のようになります

しかし、参照型を使用する限り、このようなデータ格納を行うことができません。

そこで、プリミティブ型に近い新たな型の導入が望まれたわけです。

キーとなるのは、"Codes like a class, works like an int"です。

クラスのように書けるけども、intのようにふるまうということです。

Project Valhallaでは、これを実現させるために10年に渡って議論を続けてきました。数年前までは、新たに様々な型を導入するという複雑な実現方法が提案されていました。しかし、あまりにも複雑すぎました。

そこで、去年ぐらいから、もっとシンプルな方法が検討され、やっと議論が収束してきたのです。

そして、新たに提案されたのがValue Classです。

Value Typeではなく、Value Classだというのがポイントです。

つまり、新たな型を導入するのではなく、既存の参照型の枠組みの中で特殊なクラスを導入することでCode like a class, works like an intが実現できるということです。

では、そのValue Classというのは、どのようなクラスなのでしょう?

 

Value Class

Value Classの仕様はJEP 401: Value Classes and Objectsに記述されています。

端的にいうと、Value Classをインスタンス化したオブジェクト(Value Object)にはIdentityがありません。といわれても、「Identityって何?」と思いますよね。私もそうでした。

このIdentity、Java Language SpecificationにもJVM Specificationにも明確な定義はありません。

Identityはオブジェクトを区別するために使われるオブジェクトの名前もしくはアドレスのようなものです。

具体的な値としてはSystem.identityHashCodeメソッドが返す値になります(もしくはオーバーライドしていない場合のObject.hashCodeメソッド)。

この値は、==でオブジェクト同士を比較する場合に使用されます。

また、Identityでオブジェクトを区別することが、オブジェクトの状態変更を可能にします。また、synchronizedを使用したモニタロックもIdentityを利用して実現しています。

しかし、なぜIdentityなのでしょうか?それはオブジェクトのヘッダーに関係があります。

オブジェクトヘッダー

オブジェクトヘッダーは、ヒープ上に存在するオブジェクト領域の先頭にあります。実をいうと、オブジェクトヘッダーは、JVMの実装依存でJVM Specificationには定義されていません。ここでは、OpenJDKの64bitのHotSpot VMでのオブジェクトヘッダーについて紹介します。

HotSpot VMのオブジェクトヘッダーはマークワードとクラスワードの2つのパートから構成されます。マークワードはハッシュ値、GC Age、Tagからなります。GC AgeはGCを何度経てきたかを表す回数を示します。また、Tagはマークワードがポインターで上書きされてしまうことがあるので、それを区別するために使われます。

一方のクラスワードはクラスへのポインターが格納されます。

こう見てみると、GC AgeとTagを除けば、ヘッダーによってクラスとオブジェクトを区別するための情報が格納されていることが分かります。クラスはともかく、Identityがないということはオブジェクトヘッダーがなくても大丈夫ということです。

このことから、オブジェクトヘッダーを省略してしまって、そのオブジェクトをフィールドに持つクラスに直接データを埋め込むことが可能であることを示しています。

 

ちなみに、オブジェクトヘッダーはそれなりにサイズが大きいので、小さいオブジェクトだとヘッダーの方が大きいということが起こります。そこで、Project Lilliputでオブジェクトヘッダーを小さくする仕様を策定しています。

ちょうど、次のJava 24で、LilliputのJEP 450: Compact Object HeadersがExperimentalとして導入予定です。

 

あらためてValue Class

identityが分かったところで、あらためてValue Classの定義について説明しましょう。

Value ClassはIdentityがないクラスですが、もう1つの特徴としてイミュータブルであることがあります。

Value Classではフィールドを定義すると、そのフィールドはすべて暗黙的にfinalになります。

また、Identityがないことにより、Identityを使用していた操作はできません。

たとえば、オブジェクトの比較を行う==演算は、オブジェクトの同一性ではなく、フィールドの等価性の結果を返します。つまり、equalsメソッドで比較する場合と同様になるということです。

他にも、synchronizedを使用したモニターロックや、参照を使用する弱参照(WeakReference)、ファントム参照(PhantomReference)なども使用できません。

 

Value Classの書き方

では、Value Classをどのように定義すればよいのでしょう。

これはとても簡単でclassもしくはrecordの前にvalueをつければよいだけです。

value record Point(double x, double y) {}

value class Rectangle {
    Point topleft;
    Point bottomright;

    Rectangle(Point tl, Point br) {
	topleft = tl;
	bottomright = br;
    }
}

Record Classはもともとイミュータブルなので、valueを付加しても特に問題なくコンパイルできます。

通常のクラスの場合、状態を変更するようなコードがあるとコンパイルエラーになります。

たとえば、Rectangleクラスを以下のようにセッターを追加してコンパイルしてみます。

value class Rectangle {
    Point topleft;
    Point bottomright;

    Rectangle(Point tl, Point br) {
	topleft = tl;
	bottomright = br;
    }

    public void setTopLeft(Point tl) {
	topleft = tl;
    }

    public void setBottomRight(Point br) {
	bottomright = br;
    }
}

 

> javac --release 23 --enable-preview Rectangle.java
Rectangle.java:13: エラー: final変数topleftに値を割り当てることはできません
        topleft = tl;
        ^
Rectangle.java:17: エラー: final変数bottomrightに値を割り当てることはできません
        bottomright = br;
        ^
ノート: Test.javaはJava SE 23のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
エラー2個

前述したように、Value Classのフィールドは暗黙的にfinalになるため、そこに再代入しているためコンパイルエラーになっています。

なお、ここでコンパイルに使用しているJDKはjdk.java.netで公開されているValhallaのEarly Access版です。

 

Vlaue Classが作成できるようになったので、Value Classの特徴の1つでもある==での比較を行ってみましょう。

通常のクラス(Value Classに対応してIdentity Classと呼びます)とValue Classで、JShellを使用して比較してみました。

jshell> record IDPoint(double x, double y) {}
|  次を作成しました: レコード IDPoint

jshell> var idp1 = new IDPoint(1, 2)
idp1 ==> IDPoint[x=1.0, y=2.0]

jshell> var idp2 = new IDPoint(1, 2)
idp2 ==> IDPoint[x=1.0, y=2.0]

jshell> idp1 == idp2
$4 ==> false

jshell> idp1.equals(idp2)
$5 ==> true

jshell> value record VPoint(double x, double y) {}
|  次を作成しました: レコード VPoint

jshell> var vp1 = new VPoint(1, 2)
vp1 ==> VPoint[x=1.0, y=2.0]

jshell> var vp2 = new VPoint(1, 2)
vp2 ==> VPoint[x=1.0, y=2.0]

jshell> vp1 == vp2
$9 ==> true

jshell> vp1.equals(vp2)
$10 ==> true

jshell>

Identity Classだと、フィールドの値が同一のオブジェクトであっても、==はIdentityが同じかどうかを調べるので、falseになります。その一方、Value ClassではIndentityがなく、フィールドの同値性を調べるので、==の結果はtrueになっています。もちろん、equalsメソッドで比較してもtrueです。

 

では、継承についてはどうでしょう。

Value Classは、コンクリートクラスの場合、finalクラスになるためサブクラスを作ることはできません。ただし、Value Classの抽象クラスであればサブクラスを作ることができます。

jshell> value class A {}
|  次を作成しました: クラス A

jshell> value class B extends A {}
|  エラー:
|  final Aからは継承できません
|  value class B extends A {}
|                        ^
|  エラー:
|  The concrete class A is not allowed to be a super class of the value class B either directly or indirectly
|  value class B extends A {}
|  ^------------------------^

jshell> class C extends A {}
|  エラー:
|  final Aからは継承できません
|  class C extends A {}
|                  ^

jshell> abstract value class X {}
|  次を作成しました: クラス X

jshell> value class Y extends X {}
|  次を作成しました: クラス Y

jshell> class Z extends X {}
|  次を作成しました: クラス Z

jshell>

最後のコードは抽象クラスのValue Classを継承してIdentity Classを作れるということです。まぁ、作れたとしても使うことはないでしょうけど。

もちろん、インタフェースを実装したValue Classを作ることは可能です。

jshell> interface I {}
|  次を作成しました: インタフェース I

jshell> value class J implements I {}
|  次を作成しました: クラス J

jshell>

ここで一度Value Classについてまとめておきましょう。

Value Classは以下のような特徴を持つクラスです。

  • identityのないクラス
  • イミュータブル
  • finalクラス
  • ==はフィールドの状態の比較
  • identityに依存した操作は不可
    • synchronizedを使用したモニタロック
    • 弱参照、ファントム参照などの参照

書き方に関しては以下のようになります。

  • classもしくはrecortdの前にvalueを付加して宣言
  • 抽象クラスでもValue Classにすることが可能
  • 抽象Value Classのサブクラスを定義可能
  • インタフェースの実装も可能

 

長くなってしまったので、最適化やNull-Restricted Typeについては次のエントリーで紹介していきます。

2024/09/17

JEPでは語れないJava 23

このエントリーをはてなブックマークに追加

毎度おなじみ半年ぶりのJavaのアップデートです。

Java 23はLTSのちょうど中間のリリースということもあって、それほど変化があるわけではないです。

Java 23のJEPは12ありますが、Previewばかり。PreviewでないStandard JEPは3つですがAPIの変更が伴うものはありません。

Java 23のJEPは以下の通り。

  • 455: Primitive Types in Patterns, instanceof, and switch (Preview)
  • 466: Class-File API (Second Preview)
  • 467: Markdown Documentation Comments
  • 469: Vector API (Eighth Incubator)
  • 473: Stream Gatherers (Second Preview)
  • 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal
  • 474: ZGC: Generational Mode by Default
  • 476: Module Import Declarations (Preview)
  • 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)
  • 480: Structured Concurrency (Third Preview)
  • 481: Scoped Values (Third Preview)
  • 482: Flexible Constructor Bodies (Second Preview)

Standard JEPの1つめ。JEP 467はJavadocにマークダウンが使えるというものです。

これは地味にうれしいかも。ただ、マークダウンにする時にはスラッシュ3つというのはちょっと面倒かもしれません。

IDEがマークダウンのJavadocに対応して、ショートカットでスラッシュ3つを簡単に使えるようになってほしいですね。

2つめのJEP 471は、UnsafeクラスのヒープではないネイティブメモリまわりのAPIをDeprecated for RemovalにするというJEPです。通常の用途ではUnsafeクラスは使わないとは思いますが、昔は高速化のためにヒープではなくUnsafeを使って直接メモリにアクセスしていたフレームワークやライブラリがそれなりにあったのです。

しかし、FFMが導入されたので、Unsafeを使わずにメモリアクセスできるようになったので、ようやく削除できるようになったということですね。ただ、ほんとに削除されるのがいつになるのかは微妙なところです。

Standard JEPの最後のJEP 474は、ZGCのデフォルトを世代別ZGCに変更するというものです。もともとZGCは世代別GCではなかったのですが、Java 21で世代別GCをサポートするようになりました。これに伴い、ZGCのデフォルトが世代別GCの方に変わるというものです。

ただ、これはあくまでもZGCの話で、JVMのデフォルトのGCの話ではないことに注意が必要です。

 

残りのJEPはすべてPreviewです。

JEP 455はパターンマッチングにプリミティブ型が使えるようになるというJEPです。ちょっとおもしろいのが、プリミティブ型の値と型のcaseを同じswitch式の並べて書けるところですね。まぁ、そんなに使うことはないと思いますが...

ちなみに、JEP 455は1st Previewなので、次のLTSであるJava 25に入らないんじゃないかなぁ。

 

JEP 466はバイト操作を行うためのClas-File APIです。バイトコードを直接編集してしまうわけですが、一時期ちょっとだけはやったAOPなどで使われる技術です。Class-File APIはAOPというよりは、ラムダ式の実行時に動的にクラス生成するなどJVM内部での用途が主目的のようです。

 

JEP 469 Vector APIは8回目のPreview。Project Valhallaが導入されるまで、ずーーーーっとPreviewのままです。7thからの変更点もありません。

 

JEP 473 Stream Gatherersはストリームの中間処理をもうちょっとどうにかしようというJEP。簡単にいえば、終端処理のcollectと同様のことを中間処理でもできるようにしましょうというAPIです。

これで、今までのストリームではできなかった移動平均なんかも簡単に書けるようになります。

JEP 473は2nd Previewで、すでにPreviewが外されたJEP 485が提案されているので、Java 24で正式導入ということになりそうです。

 

JEP 476 Module Import Declarationsは、インポート文をクラス単位で書くのではなくて、moduleで書けるようにしてしまいましょうというJEPです。これができると、import module java.base;で基本的なAPIは全部使えます。

これはかなり便利になりますけど、使っているクラスがどのパッケージで定義されているのか調べるにはIDEの力に頼らなければいけないという負の側面がなきにしもあらず...

JEP 476は1st Previewなので、Java 25にはまにあわないかもしれません。

 

JEP 477 Implicitly Declared Classes and Instance Main Methodsはmainメソッドを含むクラスを書かなくても、mainメソッドだけ書けばいいんじゃないというJEPです。

ちなみに、このJEPでは動的にクラスを作成していますが、その生成はバイトコード操作ではなくて、実行中にJavaのコードを動的に生成して、コンパイルとクラスロードも行うという実装になっています。

 

JEP 480 Structured ConcurrencyとJEP 481 Scoped ValuesはProject LoomでVirtual Threadsと一緒に仕様策定されていたAPIです。

両方とも3rd Previewですが、Java 23でStandardになると予想していたので、外してしまいました😱

JEP 480は複数の非同期タスクの結果を待つような処理を簡単に書けるというAPIです。CompletableFutureクラスを使えば同様の処理は書けるのですが、CompletableFutureクラスの宣言的な書き方ではなくて、手続き的な記述でも使えるよというのがポイントでしょう。

JEP 481はThreadLocalクラスの置き換えになるAPIです。ThreadLocalクラスはいろいろと問題があり、特にVirtual Threadのように多くのスレッドを使うようになるとその問題が顕現しやすくなります。

これを解決するために、JEP 481ではScopedValueクラスを導入しています。

Scoped Valueは4th PreviewがJEPのドラフトに上がっているので、Java 25に間に合うかは微妙なところです。

 

最後のJEP 482 Flexible Constructor Bodiesは、Java 22の時のJEP 447 Statements before super()の名前が変更されたJEPです。

JEP 447ではコンストラクターで、親クラスのsuper()をコールする前に処理を書けるようにしましょうというJEPでした。JEP 482ではこれに加えて、super()をコールする前に子クラスのフィールドの初期化も行えるようにしましょうというJEPになりました。

これはクラスの初期化のスキームの大きな変更なのですが、使う側からするとフィールドを先に初期化したいというニーズはほとんどないはずです。

JEPには書いてないのですが、これはProject ValhallaのVlue Classに関連しているのです。Value Classの露払いとなるJEPなのでした。

 

軽くJEPを説明したところで、本題のAPIの変更について紹介していきましょう。

 

例によって、セキュリティ関連のAPIは省略します。本バージョンでも、java.baseモジュール以外にもAPIの変更はありますが、使用頻度が低いAPIであるため、解説を省略します。

 

廃止になったAPI

Java 23では4つのクラス、5つのメソッドが削除されました。ただし、削除された4つのクラスはjava.managementモジュールのJMXに関するクラスなので、ここでは省略します。

 

メソッド

Java 22でもThreadクラスのメソッドが削除されましたが、Java 23でもスレッド関連のメソッドが削除されてインす。

  • java.lang.Thread.resume()
  • java.lang.Thread.suspend()
  • java.lang.ThreadGroup.resume()
  • java.lang.ThreadGroup.suspend()
  • java.lang.ThreadGroup.stop()

これらのメソッドはJava 14でforRemovalがtrueになっていたので、とうとう削除されたという感じですね(ThreadGroup.stopメソッドだけはJava 16です)。

それ以前からスレッドのresume/suspendは使うべきではないメソッドだったので、ようやくです。

ThreadクラスのforRemovalがtrueのメソッドは残り2つ。1つはstopメソッドですが、ThreadGroupクラスで削除されたので、Threadクラスも近いうちに削除されるような気がします。

もう1つはcheckAccessメソッドですが、こちらもいつ削除されても不思議はない感じ。

 

廃止予定のAPI

Java 23では6つのメソッドと2つのコンストラクタが廃止予定に追加されました。

  • java.io.ObjectOutputStream.PutField.write(ObjectOutput out)
  • java.net.DatagramSocketImpl.getTTL()
  • java.net.DatagramSocketImpl.setTTL(byte ttl)
  • java.net.MulticastSocket.getTTL()
  • java.net.MulticastSocket.setTTL(byte ttl)
  • java.net.MulticastSocket.send(send(DatagramPacket p, byte ttl)

PutFieldクラスのwriteメソッドの代わりは、ObjectOutputStreamクラスのwriteFieldsメソッドです。

DatagramSocketImplクラスとMulticastSocketクラスのTTLに関するメソッドは、TTLではなくTimeToLiveを使うようにします。たとえば、getTTLメソッドではなく、getTimeToLiveメソッドを使用します。

最後のMulticastSocketクラスのsendメソッドは、MulticastSocketクラスの親クラスのDatagramSocketクラスで定義されているsend(DatagramPacket p)メソッドを使用するようにします。TimeToLiveを指定するにはDatagramSocketクラスのsetOptionメソッドを使用します。

 

削除予定のコンストラクタは以下の2つです。

  • java.net.Socket(InetAddress host, int port, boolean stream)
  • java.net.Socket(String host, int port, boolean stream)

このメソッドはDatagramSocketクラスが提供される前に使われていたメソッドなのですが、DatagramSocketクラスを使うようにしましょうということです。

 

なお、java.desktopモジュールのjava.bean.beancontextパッケージもforRemovalがtrueになりました。benacontextパッケージでは18のクラスが定義されていますが、すべてforRemoval=trueになっています。

また、java.desktopモジュールに含まれるSwingのBasicSliderUI()コンストラクタも削除予定に追加されています。

 

追加/変更されたAPI

いつもの通り、Preview JEPに関するAPI変更はここでは省略します。ということで、Class-File APIなどはまた別の機会に。

 

java.base/java.ioパッケージ

JEP 477でjava.io.IOクラスが導入されるのですが(Preview APIなので、ここでは省略します)、そのIOクラスの実装で使わるConsoleクラスに多くのメソッドが追加されました。

 

Consoleクラス

Consoleクラスには7つのメソッドが追加されました。

  • Console format(Locale locale, String format, Object... args)
  • Console print(Object obj)
  • Console printf(Locale locale, String format, Object... args)
  • Console println(Object obj)
  • Console readLine(Locale locale, String format, Object... args)
  • Console readPassword(Locale locale, String format, Object... args)
  • Console readln(String prompt)

追加されたメソッドの多くは、メソッドをオーバーロードしてLocaleを指定できるようにしたものです。だいたい使い方は分かりますよね。

 

java.base/java.langパッケージ

Java 22のPreview JEPだったString Templatesがやり直しになったので、StringTemplateクラスは削除されています。また、上述したThreadクラスとThreadGroupクラスのメソッドが削除されています。

また、JEP 481 Scoped ValueのAPIが変更になっていますが、ここでは省略します。

 

java.base/java.lang.foreignパッケージ

Java 22でStandard JEPになったFFMですが、メソッドが2つ追加されました。

 

MemorySegmentインタフェース

MemorySegmentインタフェースで定義されるメソッドが1つ追加されました。

  • long maxByteAlignment()

メモリーセグメントの最大アライメントを返すメソッドです。しかし、この値を何らかの処理に使うというよりは、MemoryLayoutインタフェースのbyteAlignmentメソッドで得られる値と比較してメモリーセグメントの最大アライメントの方が小さいときには例外処理をするという使い方になります。

 

SymbolLookupインタフェース

SymbolLookupインタフェースで定義されるメソッドが1つ追加されました。

  • default MemorySegment findOrThrow(String name)

SymbolLookupインタフェースは、基本的にはfindメソッドでライブラリ内のシンボルのアドレスを探索するために使用します。findメソッドの戻り値の型はOptionalクラスで、見つからなかった場合はOptionalオブジェクトでどうにかしていました。

これに対し、Java 23で追加されたfindOrThrowメソッドを使用すると、見つからなかった場合にNoSuchElementException例外をスローします。

個人的にはOptionalクラスで見つからなかった場合に対処する方がいいとは思いますが、お好みで使い分けてください。

 

java.base/java.lang.reflectパッケージ

毎度のことですが、新しいリリースを表す定数が追加されています。

 

ClassFileFormatVersion列挙型

Java 23に対応する定数の追加です。

  • ClassFileFormatVersion RELEASE_23

 

java.base/java.lang.runtimeパッケージ

プリミティブ型の値を他の型に変換する場合に、正確であるかを調べるExactConversionsSupportクラスが追加されました。

 

ExactConversionsSupportクラス

ExactConversionsSupportクラスで定義しているメソッドは以下の21メソッドです。いずれもstaticメソッドになります。

  • static boolean isDoubleToByteExact(double n)
  • static boolean isDoubleToCharExact(double n)
  • static boolean isDoubleToFloatExact(double n)
  • static boolean isDoubleToIntExact(double n)
  • static boolean isDoubleToLongExact(double n)
  • static boolean isDoubleToShortExact(double n)
  • static boolean isFloatToByteExact(float n)
  • static boolean isFloatToCharExact(float n)
  • static boolean isFloatToIntExact(float n)
  • static boolean isFloatToLongExact(float n)
  • static boolean isFloatToShortExact(float n)
  • static boolean isIntToByteExact(int n)
  • static boolean isIntToCharExact(int n)
  • static boolean isIntToFloatExact(int n)
  • static boolean isIntToShortExact(int n)
  • static boolean isLongToByteExact(long n)
  • static boolean isLongToCharExact(long n)
  • static boolean isLongToDoubleExact(long n)
  • static boolean isLongToFloatExact(long n)
  • static boolean isLongToIntExact(long n)
  • static boolean isLongToShortExact(long n)

ExactなConvertって何だろうという感じですが、これはコードを見てみればすぐに意図が分かります。たとえば、isIntToByteExactメソッドの実装を見てみましょう。

    public static boolean isIntToByteExact(int n) {
        return n == (int)(byte)n;
    }

キャストが2つつなげて書いてあります。つまり引数のintの値をbyteにキャストして、その後にintに戻した時に元の値と同じかどうかを調べているわけです。もし、変換の時に情報が欠落するような変換であれば、2回キャストすると元の値と異なってしまうはずです。これが、Exactかどうかということです。

 

動作は分かりましたが、なぜこんなクラスが今になって追加されたのですね。理由は単純でJEP 455 Primitive Types in Patterns, instanceof, and switchのためです。

パターンマッチングでプリミティブ型が使えるようになりましたが、その時に値を正確に変換できるかどうかが重要になるからです。たとえば、下のコードで考えてみましょう。

    int x = ...;
	
    var y = switch (x) {
        case 0 -> "0";
        case byte a -> "Byte " + a;
        case int b -> "Int " + b;
    };

このコードでは、xの値が-128から127であれば、byteのcaseにマッチします。それを超える範囲、たとえば128だとbyteの範囲を超えるので、intのcaseにマッチします。

つまり、正確に変換が行えるのであれば、型が異なっていてもマッチするわけです。この正確な変換ができるかどうかをチェックするためにExactConversionsSupportクラスが使われるのです。

型の変換に関してはJEP 455にも記述があるので、参考にしてみてください。

 

java.base/java.netパッケージ

Java 22でInet4Address/Inet6Addressクラスのファクトリメソッドが追加されましたが、Java 23ではInet4Addressクラスにファクトリメソッドがさらに追加されました。

 

Inet4Addressクラス

Inet4AddressクラスにアドレスをPosixのリテラルで指定できるファクトリメソッドが追加されました。

  • static Inet4Address ofPosixLiteral(String posixIPAddressLiteral)

ofLiteralメソッドでは10進数でアドレスを表記しますが、ofPosixLiteralメソッドでは8進数や16進数も使用することができます。

 

java.base/java.textパッケージ

数値をフォーマットするNumberFormatクラスと、そのサブクラスにフォーマットの厳密さを指定するメソッドが追加されました。

 

NumberFormatクラス

NumberFormatクラスのパースはデフォルトでは寛大になっています。これに対し、厳密なパースに関するメソッドが追加されました。

  • boolean isStrict()
  • void setStrict(boolean strict)

NumberFormatクラスでは、これらのメソッドをコールするとUnsupportedOperationException例外がスローされます。

厳密なパース処理は、以下の3種類のサブクラスで使用することができます。

  • ChoiceFormat
  • CompactNumberFormat
  • DecimalFormat

それぞれのクラスのparseメソッドのAPIドキュメントに厳密な場合について記述されているので、参考になさってください。まぁ、それほど使うとは思わないですけどw

 

java.base/java.timeパッケージ

時点を表すInstantメソッドにメソッドが1つオーバーロードされました。

 

Instantクラス

Instantクラスには時間量を調べるuntilメソッドがありましたが、オーバーロードされています。

  • Duration until(Instant endExclusive)

既存のuntilメソッドはもう一方の時点をTemporalオブジェクトで指定し、戻り値はlongで表されます(どの時間量なのかは第2引数で指定します)。

これはちょっと使いにくいので、時点をInstantオブジェクトで表し、戻り値は時間間隔を表すDurationオブジェクトで表されるuntilメソッドのオーバーロードが追加されたわけです。

 

その他

なんとAPIの変更はこれだけなのです。なのですが、他にちょっとだけ気になる変更があったので、それも一緒に紹介しておきます。

 

COMPATロケールプロバイダーの廃止

ロケールプロバイダーってなんだという感じですが、ロケールのデータベースのようなものです。

Javaでは歴史的経緯から3種類のロケールプロバイダーを提供していました。しかし、Java 9からは世界的な標準であるCommon Locale Data Repository (CLDR)がデフォルトになっています。

そして使われなくなった残り2つのロケールプロバイダー(JREとCOMPAT)が削除されることになりました。

Java 21から、JREかCOMPATを使っていると警告が表示されていたのですが、早々に削除されることになりました。

詳しくはJava Bug Systemの JDK-8325568 をご覧ください。

 

標準Docletの変更

DocletというのはJavaのソースコードからJavadocを生成するためのツールです。

JEP 467でJavadocの見直しがあったためなのかどうか分かりませんが、標準Docletが変更されJavadocの見た目が変わりました。

具体的には下図のように左側にサイドバーが出るようになっています。以前のように階層をたどるためのサイドバーではなく、右側に表示しているクラスやインタフェースの目次的なサイドバーになっています。

しかし、これが微妙なんですよね。メソッドの一覧がソートされておらず、書いてある順になっているので探しにくいのです。Method Summaryの表のようにソートされないですかねぇ。

 

というわけで、Java 23のAPI変更について紹介してきました。

Java 23のAPIの変更は少なかったのですが、次のJava 24では大幅にAPIが追加されそうです。

というのも、すでにPreviewではなくなったJEP 484 Class-File APIやJEP 485 Stream Gatheresが提案されているからです。この2つのJEPはまだターゲットリリースが記述されていませんが、Java 24になるのは既定路線でしょう。

2024/03/19

JEPでは語れないJava 22

このエントリーをはてなブックマークに追加

毎度おなじみ半年ぶりのJavaのアップデートです。

Java 22は、LTSであるJava 21の次のバージョンですが、意外と新機能盛りだくさんです。

Java 22のJEPは以下の12。しかも、スタンダードJEPが4もあります。

  • 423: Region Pinning for G1
  • 447: Statements before super(...) (Preview)
  • 454: Foreign Function & Memory API
  • 456: Unnamed Variables & Patterns
  • 457: Class-File API (Preview)
  • 458: Launch Multi-File Source-Code Programs
  • 459: String Templates (Second Preview)
  • 460: Vector API (Seventh Incubator)
  • 461: Stream Gatherers (Preview)
  • 462: Structured Concurrency (Second Preview)
  • 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)
  • 464: Scoped Values (Second Preview)

注目すべきは、長らくIncubatorやPreviewだったJEP 454。FFMと省略して呼ぶことがおおいですが、Project Panamaのメインとなる機能です。

JNIの代わりに、ネイティブコードをコールしたり、ヒープ外のメモリにアクセスするためのAPIです。

モジュールはjava.baseで、パッケージはjava.lang.foreignになります。

APIなので、本来であれば本エントリーでも取り上げるのですが、ちょっと量が多いですし、差分を紹介してもしかたありません。そこで、別エントリーで使い方についてまとめて紹介する予定です。

ちなみに、同じくProject Panamaで仕様策定しているVector APIはまだIncubatorのままですが、次のバージョンで正式にリリースされるのではないかというのが、さくらばの予想です。

言語仕様の変更がJEP 447, 456, 459, 463と4種類もあります。JEP 456だけがスタンダードJEPで使用しない変数やパターンを _ (アンダーバー)で省略して記述できるというものです。

スタンダードJEPであるJEP 423はG1GCのアルゴリズム改良、JEP 458はjavacでコンパイルすることなく複数のJavaコードを実行できるというものです。この機能は、JEP 330の拡張ですね。

あらたにPreview JEPになったのが、JEP 457とJEP 461です。

JEP 457はバイトコードを扱うためのAPIです。今までバイトコード操作というと、ASMなどが使われていましたが、標準のAPIで可能になります。

JEP 461はStream APIの拡張です。今まで中間操作はストリームの流れてくる1データに対する処理に限定されていましたが、Gathereを使用するとかなり柔軟に中間操作を記述することができるようになります。

 

と、軽くJEPを説明したところで、APIの変更について紹介していきましょう。JEPは多いのですが、意外にもAPIの変更は少ないです。ほとんどがJEP 454とJEP 457に関する変更です。ただし、今回もPreviewやIncubatorの変更は省略するので、JEP 457に関連したAPI変更はStandard JEPになった時に紹介します。また、前述したようにJEP 454 FFMは別エントリーで紹介する予定です。

例によって、セキュリティ関連のAPIは省略します。本バージョンでも、java.baseモジュール以外にもAPIの変更はありますが、使用頻度が低いAPIであるため、解説を省略します。

 

廃止になったAPI

Java 22では1つのメソッドが廃止になりました。しかし、もともと使用しても例外をスローする実装になっているので、廃止されても問題はないはずです。

 

メソッド

  • java.lang.Thread.countStackFrames()

スタックフレームをカウントするメソッドですが、Java 21まではUnsupportedOperationException例外をスローする実装になっています。

 

廃止予定のAPI

Java 22で追加された廃止予定のAPIはありません。

 

追加/変更されたAPI

Java 22のjava.baseモジュールで追加されたAPIは約300なのですが、そのうちの200以上がJEP 457 Class-File APIで、約30がJEP 454 FFM APIです。8割ぐらいは、この2つのJEP由来の変更ということになります。本エントリーでは残りの2割を紹介していきます。。

 

java.base/java.ioパッケージ

java.ioパッケージのConsoleクラスで1つだけメソッドが追加されました。

 

Consoleクラス

Consoleオブジェクトで扱っているデバイスがターミナルかどうかを調べるメソッドが追加されました。

  • static boolean isTerminal()

ターミナルというのは標準入出力に対応したデバイス(POSIXでいうところのtty)です。ターミナルであればtrueが返ります。逆にいうと、JShellやIDEのコンソールだとfalseになります。

jshell> System.console().isTerminal()
$1 ==> false

jshell>

 

java.base/java.langパッケージ

Java 22では、Unicode 15.1に対応したのでそれに応じたブロックの追加が行われました。これ以外にClassクラスとStackWalker.Option列挙型に追加があります。

 

Character.UnicodeBlockクラス

Unicode 15.1で追加されたブロックの定数が追加されました。

  • static final Character.UnicodeBlock CJK_UNIFIED_IDEOGRAPHS_EXTENSION_I

 

Classクラス

プリミティブ型に対応するClassオブジェクトを取得するメソッドが追加されました。

  • static Class<?> forPrimitiveName(String primitiveName)

引数にはプリミティブ型を表す文字列、たとえば"int"とか"double"を指定します。引数がnullの場合、NullPointerException例外がスローされます。

 

StackWalker.Option列挙型

StackWalkerクラスはスレッドごとに作成されるスタックフレームを操作するクラスです。Option列挙型はStackWalkerオブジェクト生成時に使用する列挙型ですが、定数が1つ追加されました。

  • StackWalker.Option DROP_METHOD_INFO

StackWalkerオブジェクトがスタックフレームを操作する時にメソッドの情報を扱わないように指定します。

 

java.base/java.lang.reflectパッケージ

いつものことですが、新しいリリースを表す定数が追加されています。

ClassFileFormatVersion列挙型

Java 22に対応する定数の追加です。

  • ClassFileFormatVersion RELEASE_22

 

java.base/java.netパッケージ

IPv4/IPv6のアドレスを表すInet4Addressクラス/Inet6Addressクラスは直接生成することはできず、スーパークラスのファクトリメソッドを使用していました。これに対し、それぞれのクラスにファクトリメソッドが追加されました。

 

InetAddressクラス

"127.0.0.1"などに対応するInetAddressオブジェクトを生成するにはセグメントの配列を使用するgetByAddressメソッドか、ホスト名も使用できるgetByNameメソッドを使用してきました。これに対し、アドレスを文字列で指定するファクトリメソッドが追加されました。

  • static InetAddress ofLiteral(String ipAddressLiteral)

ofLiteralメソッドでは、引数の文字列をまずIPv4と仮定してパースを行います。失敗した場合、IPv6としてパースします。パースに失敗するとIllegalArgumentException例外がスローされます。

実際の処理はInet4AddressクラスおよびInet6Addressクラスに委譲します。

 

Inet4Addressクラス

Inet4Addressクラスにもアドレスを文字列で指定するファクトリメソッドが追加されました。

  • static Inet4Address ofLiteral(String ipv4AddressLiteral)

アドレスの表記は今まで使用してきたのと同じです。d.d.d.d形式だけでなく、d.d.dからd.d、そしてd形式もパース可能です。

jshell> Inet4Address.ofLiteral("127.0.0.1")
$1 ==> /127.0.0.1

jshell> Inet4Address.ofLiteral("127.0.1")
$2 ==> /127.0.0.1

jshell> Inet4Address.ofLiteral("127.0.257")
$3 ==> /127.0.1.1

jshell>

 

Inet6Addressクラス

Inet6Addressクラスも同様にファクトリメソッドが追加されました。

  • static Inet6Address ofLiteral(String ipv6AddressLiteral)

アドレスの表記も従来と同じで、::や::d.d.d.d形式なども使用できます。

jshell> Inet6Address.ofLiteral("::1")
$1 ==> /0:0:0:0:0:0:0:1

jshell>

 

java.base/java.nio.charsetパッケージ

UTF-8などの標準的な文字セットを定数に持つStandardCharsetsクラスに定数が追加されました。

 

StandardCharsetsクラス

StandardCharsetsクラスではUTF-8やUTF-16系の定数は定義されていましたが、UTF-32系がなかったので追加されました。

  • static final Charset UTF_32
  • static final Charset UTF_32BE
  • static final Charset UTF_32LE

 

java.base/java.nio.fileパッケージ

Pathインタフェースにデフォルトメソッドが追加されました。

 

Pathインタフェース

Pathインタフェースにresolveメソッドのオーバーロードが2種類追加されました。いずれもデフォルトメソッドです。

  • default Path resolve(String first, String... more)
  • default Path resolve(Path first, Path... more)

実際の動作はfirstに対しresolveを行い、得られたPathオブジェクトに対しmoreを順々にresolveしていきます。

実際のコードは以下のようになっています。

    default Path resolve(Path first, Path... more) {
        Path result = resolve(first);
        for (Path p : more) {
            result = result.resolve(p);
        }
        return result;
    }

 

java.base/java.textパッケージ

リストをフォーマットするクラスが追加されました。

 

ListFormatクラス

ListFormatクラスはリストのフォーマッタークラスです。なぜになって導入されたのか、いまいち謎です。

他のフォーマッターと同様にスタイルなどを指定する列挙型も導入されています。

  • enum ListFormat.Style { FULL, SHORT, NARROW }
  • enum ListFormat.Type { STANDARD, OR, UNIT }

ListFormatクラスの使い方は他のフォーマッタークラスと同じです。getInstanceメソッドでListFormatオブジェクトを生成し、フォーマットするのであればformatメソッド、パースをするのであればparseメソッドを使用します。

主なメソッドを以下に示します。

  • static ListFormat getInstance()
  • static ListFormat getInstance(Locale locale, ListFormat.Type type, ListFormat.Style style)
  • String format(Object obj)
  • String format(List<String> input)
  • List<String> parse(String source)
  • Object parseObject(String source)

Objectクラスを引数にするformatメソッドと、parseObjectメソッドはFormatクラスで定義されたメソッドです。

また、引数のないgetInstanceメソッドはデフォルトロケール、STANDARD、FULLとなります。

StyleとTypeによるフォーマットの違いはListFormatクラスのJavadocにまとめられているので、参考にしてください。

個人的には日本語ロケールだと、ちょっと使いものにならない気が...

jshell> import java.text.*

jshell> var format = ListFormat.getInstance()
format ==> ListFormat [locale: "日本語 (日本)", start: "{0}、{1}", ... }", three: "{0}、{1}、{2}"]


jshell> format.format(List.of(0, 1, 2, 3))
$3 ==> "0、1、2、3"

jshell> format.format(List.of("a", "b", "c"))
$4 ==> "a、b、c"

jshell>

ここで示したようにデフォルトの日本語ロケールだと、リストの区切り文字に全角の"、"が使われます。それはちょっとなぁと思うわけです。

これに対し、たとえばUSロケールでSTANDARD/FULLだと次のようになります。

 jshell> var format = ListFormat.getInstance(Locale.US, ListFormat.Type.STANDARD, ListFormat.Style.FULL)
format ==> ListFormat [locale: "英語 (アメリカ合衆国)", start: "{0},  ... ree: "{0}, {1}, and {2}"]


jshell> format.format(List.of(0, 1, 2, 3))
$6 ==> "0, 1, 2, and 3"

jshell>

英語的には最後の要素が", and "となるのは分かるのですが、これを使いたいことがあるのでしょうか。

結局、よく使うのはTypeをSTANDARDではなくUNITにし、StyleはFULLかSHORTのような気がします。

 jshell> var format = ListFormat.getInstance(Locale.of("c"), ListFormat.Type.UNIT, ListFormat.Style.FULL)
format ==> ListFormat [locale: "c", start: "{0}, {1}", middl ... , three: "{0}, {1}, {2}"]

jshell> format.format(List.of(0, 1, 2, 3))
$8 ==> "0, 1, 2, 3"

jshell>

parseメソッドは戻り値の型がList<String>となることに注意してください。

 

java.base/java.util.concurrentパッケージ

Fork/Join Framework関連でメソッドが追加されました。いずれも割り込みに関するメソッドです。

 

ForkJoinPoolクラス

割り込みがかからないタスク実行のメソッドが追加されています。

  • <T> List<Future<T>> invokeAllUninterruptibly(Collection<? extends Callable<T>> tasks)

複数のタスクをまとめて実行する時に使用するのがinvokeAllメソッドですが、それに割り込みがかからないようにしたのがinvokeAllUninterruptibly()メソッドです。

このメソッドではタスクをjoinする時に、quitelyJoinメソッドを使用しているため、割り込みがかからないようになっています。

 

ForkJoinTaskクラス

ForkJoinPoolクラスとは逆に、割り込みがかかるタスクのファクトリーメソッドが追加されました。

  • static <T> ForkJoinTask<T> adaptInterruptible(Callable<? extends T> callable)
  • static <T> ForkJoinTask<T> adaptInterruptible(Runnable runnable, T result)

今までのadoptメソッドでタスクを生成した場合、タスクに割り込みをかけることができませんでした。これに対し、adaptInterruptibleメソッドではタスクに対して割り込みを書けることができます。

戻り値の型はForkJoinTaskクラスですが、実際には派生クラスのInterruptibleTaskクラスのさらに派生クラスであるAdaptedInterruptibleCallableクラスが戻ります。

2種類のオーバーロードの違いは、引数の型が違うのでjoinした時の戻り値に違いがでるということです。Runnableインタフェースではタスクの戻り値がないので、adaptInterruptibleメソッドの第2引数のresultが返ります。

 

java.base/java.util.randomパッケージ

乱数値のストリームを生成するメソッドが追加されています。

 

RandomGeneratorクラス

RandomGeneratorクラスではdoubleの乱数値のストリームを生成するdoublesメソッドがあります。これの派生メソッドが追加されました。

  • default DoubleStream equiDoubles(double left, double right, boolean isLeftIncluded, boolean isRightIncluded)

doublesメソッドでは要素数を指定しますが、equiDoublesメソッドは無限ストリームになります。

引数は境界値で、その境界値を含むかどうかを第3, 4引数で指定します。

 

 

Java 22のAPI変更について紹介しましたが、やはり少ないですね。

また、これは便利だとか、使えそうというAPIの追加もないようです。

とはいうものの、FFMは外部ライブラリを使いたい人には有用ですし、Class-File APIもASMを使っていた人にはうれしいはず。といっても、これらを使う開発者はごくごくわずかだとは思います。

普通の開発者であれば、ストリームのGathererは便利に使えるはずです。GathererがStandard JEPになるまで、待ちましょう!

 

さて、次のJava 23では、長らくIncubatorだったVector APIが入るかどうかです。最近はVector APIのAPI変更もないようなのですが、一波乱あるのかどうか。ぜひ入ってほしいなぁ。