2026/04/06

JEPで語るJava 26

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

gihyo.jpの記事や、JavaOneの参加レポート(前編後編)などを書いていたら、JEPで語るが4月になってしまいました😰

Java 26のJEPは10。そのうちの半分がStandard JEPです。LTSの次のバージョンにしては多いですね。

Java 26のJEPの一覧はこちら。

  • 500: Prepare to Make Final Mean Final
  • 504: Remove the Applet API
  • 516: Ahead-of-Time Object Caching with Any GC
  • 517: HTTP/3 for the HTTP Client API
  • 522: G1 GC: Improve Throughput by Reducing Synchronization
  • 524: PEM Encodings of Cryptographic Objects (Second Preview)
  • 525: Structured Concurrency (Sixth Preview)
  • 526: Lazy Constants (Second Preview)
  • 529: Vector API (Eleventh Incubator)
  • 530: Primitive Types in Patterns, instanceof, and switch (Fourth Preview)

 

では、1つずつ紹介していきましょう。

 

JEP 504: Remove the Applet API

JEPのタイトル通り、Appletに関するAPIを削除するJEPです。

Applet Viewerなどのアプレットを動作させる方はとっくに削除されていましたが、APIはまだ残っていたのでした。

java.appletパッケージが削除されただけではなく、SwingのJAppletクラスも削除されています。

実際にはAWTやJava 2Dの内部でアプレットに関連した部分がいろいろと削除されているようです。

 

JEP 500: Prepare to Make Final Mean Final

finalをfinalにしましょうというJEPです。

「どういうこと?」と思われるかもしれませんが、今はfinalは実はfinalではないのです。

たとえば、次のプログラムを実行してみるとどうなるでしょう。

class Foo {
    final int x;

    public Foo() {
        x = 0;
    }
}

void main() throws Exception {
    var foo = new Foo();
    IO.println("Initla Value: " + foo.x);

    var clazz = Foo.class;
    var refX = clazz.getDeclaredField("x");

    // おまじない
    refX.setAccessible(true);
    
    // 値の変更
    refX.set(foo, 10);
    IO.println("Modified Value: " + foo.x);
}

リフレクションでFooクラスのxフィールドの値を書き換えているプログラムです。

このプログラムをJava 25で実行してみましょう。

C:\src> C:\Program Files\Java\jdk-25\bin\java -version
openjdk version "25" 2025-09-16
OpenJDK Runtime Environment (build 25+36-3489)
OpenJDK 64-Bit Server VM (build 25+36-3489, mixed mode, sharing)

C:\src> C:\Program Files\Java\jdk-25\bin\java Main.java
Initla Value: 0
Modified Value: 10

C:\src>

あっさりと値の書き換えができてしまいました。

ここでキーになるのはsetAccessible()メソッドです。setAccessible()メソッドの引数にtrueを指定してコールすると、finalフィールドへの代入、privateメソッドのコール、privateフィールドの柿替えなどができてしまいます。

つまり、finalフィールドであってもfinalではないという状況を作り出してしまったわけです。

 

このようにfinalフィールドの書き換えができるということをやめましょうというのが、JEP 500です。

ただし、いきなり禁止にしてしまうと、動作できなくなるアプリケーションも出てくるので、まずは警告を出すようにして、将来的には原則禁止にしていきましょうということです。

 

では、同じプログラムをJava 26で実行してみましょう。

C:\src> C:\Program Files\Java\jdk-26\bin\java -version
openjdk version "26" 2026-03-17
OpenJDK Runtime Environment (build 26+35-2893)
OpenJDK 64-Bit Server VM (build 26+35-2893, mixed mode, sharing)

C:\src> C:\Program Files\Java\jdk-26\bin\java Main.java
Initla Value: 0
WARNING: Final field x in class Main$Foo has been mutated reflectively by class Main in unnamed module @4c6e276e (file:/C:/Main.java)
WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning
WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled    
Modified Value: 10

C:\src>

値の書き換えはできるものの、Warningが出るようになりました。これがJEP 500によるものです。

 

さて、リフレクションによるfinalフィールドの値書き換えができなくなるのはいいのですが、ライブラリやフレームワークがリフレクションを使っていて警告が出てしまう場合はどうすればよいでしょう?

対応策は2種類あります。どちらも実行時オプションで指定します。

  • モジュール単位でリフレクションによるfinal変更を許可する
  • 全体の動作を指定する

1つめのモジュール単位で許可する方法は、上記のWarningの文章の中にもある--enable-final-field-mutationオプションです。

--enable-final-field-mutation=m1,m2 のようにの後に許可するモジュールをカンマ区切りで列挙します(m1とm2がモジュールです)。

すべてに許可する場合は --enable-final-field-mutation=ALL-UNNAMED とします。

 

もう一方の全体の動作を指定するには、--illegal-final-field-mutationオプションで行います。

指定できるのは、以下の4種類です。

  • allow : finalの書き換えを許可
  • warn : finalの書き換えがあると警告を出力 (デフォルト)
  • debug : finalの書き換えがあると警告とスタックトレースを出力
  • deny : finalの書き換えを禁止

Java 26でのデフォルトはwarnです。しかし、将来的にはdenyがデフォルトになる予定です。

denyがデフォルトになる前に、警告が出たら対応しておきましょう。

 

JEP 517: HTTP/3 for the HTTP Client API

java.net.httpモジュールで提供されているHTTP ClientをHTTP/3に対応させるというJEP。

HTTP/3はTCPではなく、UDPで通信するのですが、ブラウザーで使っていると違いは全然分からないですね。同じようにHTTP Clientでも、既存の使い方とまったく変わらずに通信することができます。

違いは、HTTPのバージョン指定でHTTP/3にすることだけです。

このために、HTTPのバージョンを表すHttpClient.Version列挙型に定数が追加されています。

  • HTTP_3

後は、HttpClientオブジェクトを生成するときにバージョンを指定するだけです。もしくは、リクエストごとに指定することも可能で、その場合はHttpRequestオブジェクトを生成するときにバージョンを指定します。

前者であれば、次のように記述します。

    var client = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_3)
            .build();

後者のリクエストで指定するには、次のようになります。

    var request = HttpRequest.newBuilder()
            .uri(URI.create("https://google.com/"))
            .version(HttpClient.Version.HTTP_3)
            .GET()
            .build();

後は、通常の使い方と同じです。

ただし、実際にHTTP/3が使われるかどうかは、Webサーバーとのネゴシエーションによるので、HTTT/3を指定したけどHTTP/2で通信していたということもあります。

 

ただ、動作がよく分からないこともあります。たとえば、google.comに接続するときにHttpClientオブジェクトでバージョン指定してもHTTP/2が使われます。しかし、HttpRequestオブジェクトにバージョン指定するとHTTP/3で通信できます。

何かが違うのでしょうけど、イマイチよくわからず...

 

JEP 516: Ahead-of-Time Object Caching with Any GC

Standard JEPの残り2つはGC関連。ただし、JEP 516はGCというよりは、Project Leydenです。

起動時間やウォームアップ時間を短縮するために、Project LeydenではAOTキャッシュファイルを使用します。

AOTキャッシュにはロードしたクラスや、HotSpotVMのプロファイリング解析結果などを保存しておきます。

また、AOTキャッシュでは初期化したオブジェクトも保存して置くことが可能です。ここで、GCとのやり取りが出てくるわけです。

JEP 516はタイトルにAny GCとありますが、実をいうとすでにG1 GCやParallel GC、Serial GCはAOTキャッシュに対応済みでした。

しかし、ZGCは対応ができていませんでした。そこで、GCに依存しような形式でオブジェクトをキャッシュできるようにしましょうというのがJEP 516です。ただし、GCに非依存にするため、効率は若干落ちてしまいます。

このため、今まで対応できていなかったZGCやShenandoah GCなどを使用する時だけ、GC非依存フォーマットにする方がよさげです。

GCに非依存のオブジェクトキャッシュを使用する場合は、AOTキャッシュを作成する時に-XX:+AOTStreamableObjectsオプションを指定します。

 

JEP 522: G1 GC: Improve Throughput by Reducing Synchronization

G1 GCではGCのたびにオブジェクトが領域を移動し、このためオブジェクト参照先のオブジェクトのアドレスが変更されます。この参照をたどることを効率的に行うためにカードテーブルというテーブルを使用します。

このカードテーブルはGCのスレッドだけでなく、アプリケーションスレッドでも使用します。

また、G1 GCではレイテンシーを改善するために、GCスレッドとアプリケーションスレッドが並列に動作する時間が長くなります。

つまり、GCスレッドとアプリケーションスレッドの両方からアクセスされるカードテーブルは、アクセスする時に同期化が必要になるということです。

この同期化によってパフォーマンスが低下してしまうことありました。それを改善しましょうというのが、JEP 522です。

 

では、どうやって解決するかというと、カードテーブルを2つ用意して、一方を読み込み、他方を書き込み専用にしてしまおうという手法。まぁ、CopyOnWriteArrayListクラスのようなものです。

とはいうものの、従来の手法でもロック取得待ちが発生することはそれほどないはずです。よっぽどヒープが逼迫して、頻繁にGCが発生するような場合でもなければ、JEP 522の恩恵に預かることはないかもしれません。

 

Preview/Incubator JEP

ここからはお試し機能のPreview/Incubator JEPなので、簡単に触れるだけにしましょう。

いずれも、まだ変更が入る可能性があるので、その点はご注意ください。

 

JEP 524: PEM Encodings of Cryptographic Objects (Second Preview)

PEMエンコードは証明書や鍵交換で使用されるバイナリをBase64でテキスト化するエンコードです。

Java 26ではPEMRecordクラスがPEMクラスに変更されたなどがあります。

そして、次のPEMエンコードのJEPでStandard JEPになるようです。今のところ、ターゲットとなるバージョンが確定していないのですが、Java 27で正式になるでしょう。

 

JEP 525: Structured Concurrency (Sixth Preview)

6回目のプレビューでなかなかStandard JEPにならないStructured Concurrencyです。

複数のタスクをパラレルに処理させて、その結果をまとめるという用途で使用します。

Java 26ではタイムアウトを設定できるようになったり、結果をストリームで返していたのを、リストにするなどの変更がありました。

次のJEPもドラフトになっているのですが、まだプレビューのままのようです。

 

JEP 526: Lazy Constants (Second Preview)

Lazy Constantsは、Java 25ではStable ValuesだったAPIです。

Lazy Constantsについては、JJUG CCC 2025 Fallでプレゼンしましたし、解説ブログも書きましたので、そちらをご参照ください。

 

 

JEP 530: Primitive Types in Patterns, instanceof, and switch (Fourth Preview)

intやdoubleなどのプリミティブ型をパターンマッチングで使用できるようにしようというのがJEP 530です。

こちらもなかなかStandard JEPにならないですねぇ。

プリミティブ型は暗黙の型変換が絡むので、そんなこともなかなかStandard JEPにならない要因になっている気がします。

実際にJava 26でも型変換に関して厳密になるような変更が加えられています。

そして、次のJEPもまたPreview JEPになっています。ただし、次のJEPでは変更がないので、このまま行けばJava 28でStandard JEPになりそうです。

 

JEP 529: Vector API (Eleventh Incubator)

Value Classが導入されるまでIncubator JEPのままが確定しているVector APIですが、Java 26でも変更はありません。

このままずっと塩漬け状態が続くのかと思っていたのですが、内部的にはいろいろと変更があるようです。というのもJavaOneでVector APIのセッションがあったのですが、そこでどういうことを行っているかについて説明があったからです。

資料は公開されているので、参考までに。

 

まとめ

Java 26の10のJEPについて簡単に解説しました。

Standard JEPでAPIが変更されるのはJEP 517だけですが、他のJEPは安全性やパフォーマンスの強化が図られています。

だからといって、Java 26をインストールしましょうというほどではないですね。次のLTSまでの間にこのような強化が積み重なっていけばよいと思います。

次のJava 27はまだJEPが1つだけですが、ドラフトにいろいろ上がってきているのでどれがJava 27をターゲットにするのか楽しみですね。

2026/03/17

JEPでは語れないJava 26

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

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

Java 26はLTSの次のバージョンということで、新機能も少なめ。APIの変更も少ししかありません。まぁ、そんなもんですね。

Java 26のJEPの一覧はこちら。

  • 500: Prepare to Make Final Mean Final
  • 504: Remove the Applet API
  • 516: Ahead-of-Time Object Caching with Any GC
  • 517: HTTP/3 for the HTTP Client API
  • 522: G1 GC: Improve Throughput by Reducing Synchronization
  • 524: PEM Encodings of Cryptographic Objects (Second Preview)
  • 525: Structured Concurrency (Sixth Preview)
  • 526: Lazy Constants (Second Preview)
  • 529: Vector API (Eleventh Incubator)
  • 530: Primitive Types in Patterns, instanceof, and switch (Fourth Preview)

 

10のJEPのうち、半分はPreviewとIncubatorです。

Standard JEPでは、JEP 500は予告のようなものでfinalは値の変更をできなくするよというもの。JEP 504はとうとうAppletのAPIが削除されるというもの。

残りの3つのStarndard JEPも、APIの変更はほとんどなし。主にパフォーマンスに関するJEPです。

Standard JEP以外のJEPも、新しいものはありません。そろそろStandard JEPになってもいいんじゃないかなぁというのもありますね。

これらのJEPに関しては、次エントリーで紹介する予定です。

 

さて、JEPで語れない方です。Java 26はAPIの変更も少なめ。

とはいうものの、java.baseモジュール以外の変更もあるので、そちらも紹介します。しかし、いつものごとくセキュリティ関連は省略させてください。

また、今回からバージョンに関する定数の追加についても省略します。

 

廃止になったAPI

Java 26ではJEP 504でアプレットに関するAPIがごそっと削除されました。

パッケージ

  • java.applet

java.appletパッケージで定義されていたインタフェース、クラスなども削除されています。

 

クラス

  • javax.swing.JApplet

Swingでアプレットを作るときに使われたJAppletクラスも削除です。

 

メソッド

アプレット関連以外にも削除されたメソッドが多いので、注意が必要です。とはいえ、finalize()メソッドなど基本的には使われていないメソッドのはず。

そして、とうとうThread.stop()メソッドも削除されました。

  • java.beans.Beans.instantiate(ClassLoader,String,BeanContext,AppletInitializer)
  • java.lang.Thread.stop()
  • java.net.DatagramSocketImpl.getTTL()
  • java.net.DatagramSocketImpl.setTTL(byte)
  • java.net.MulticastSocket.getTTL()
  • java.net.MulticastSocket.send(DatagramPacket, byte)
  • java.net.MulticastSocket.setTTL(byte)
  • javax.imageio.spi.ServiceRegistry.finalize()
  • javax.imageio.stream.FileCacheImageInputStream.finalize()
  • javax.imageio.stream.FileImageInputStream.finalize()
  • javax.imageio.stream.FileImageOutputStream.finalize()
  • javax.imageio.stream.ImageInputStreamImpl.finalize()
  • javax.imageio.stream.MemoryCacheImageInputStream.finalize()
  • javax.management.modelmbean.DescriptorSupport.toXMLString()
  • javax.swing.RepaintManager.addDirtyRegion(Applet, int, int, int, int)

 

例外

  • javax.management.modelmbean.XMLParseException

 

コンストラクター

  • javax.management.modelmbean.DescriptorSupport.<init>()

 

廃止予定に追加されたAPI

Java 26でもセキュリティマネージャーの削除に関連して、パーミッション系のクラスがforRemoval=trueになっています。

クラス

  • java.net.SocketPermission
  • java.sql.SQLPermission

 

メソッド

  • java.lang.classfile.Signature.ClassTypeSig.of
  • java.net.ServerSocket.setPerformancePreferences
  • java.net.Socket.setPerformancePreferences
  • java.net.SocketImpl.setPerformancePreferences

 

追加されたAPI

Java 25で大幅に変更されたAPIは少ないのですが、JEP 517のHTTP/3導入にともなってjava.net.httpモジュールに追加があるのが大きなところでしょうか。

また、JDBCにAPIが追加されているのが珍しいですね。

 

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

Java 26ではサポートしているUnicdeのバージョンが17.0になりました。これに伴って、Characterクラス関連で定数が多く追加されています。

 

Character.UnicodeBlockクラス

Unicodeのブロックが追加されたことに対応して、定数が追加されています。

  • BERIA_ERFE
  • CJK_UNIFIED_IDEOGRAPHS_EXTENSION_J
  • MISCELLANEOUS_SYMBOLS_SUPPLEMENT
  • SHARADA_SUPPLEMENT
  • SIDETIC
  • TAI_YO
  • TANGUT_COMPONENTS_SUPPLEMENT
  • TOLONG_SIKI

 

Character.UnicodeScript列挙型

同様にスクリプトも追加されているので、対応する定数が追加されています。

  • BERIA_ERFE
  • SIDETIC
  • TAI_YO
  • TOLONG_SIKI

 

Processクラス

外部のプログラム実行に使用されているProcessクラスですが、Closeable/AutoCloseableインタフェースを実装するようになりました。

これでやっとtry-with-resources構文で使えるようになりました。

  • close()

 

Stringクラス

Unicodeのケースフォールディングに対応した比較メソッドが追加されています。

ケースフォールディングでは、アルファベットの大文字、小文字を区別しないだけでなく、ドイツ語のßとssなどを区別しません。

Latin-1の文字であればequalsFoldCase()メソッドとequalsIgnoreCase()メソッドは同じ結果を返しますが、Latin-1以外の文字で結果が異なる場合があるということですね。

定数のUNICODE_CASEFOLD_ORDERはケースフォールドに対応したComparator<String>オブジェクトです。

 

  • int compareToFoldCase(String)
  • boolean equalsFoldCase(String)
  • Comparator<String> UNICODE_CASEFOLD_ORDER

ケースフォールディングで比較するequalsFoldCaseメソッドを試してみましょう。

jshell>  "Hello, World!".equalsIgnoreCase("hello, world!")
$1 ==> true

jshell> "Hello, World!".equalsFoldCase("hello, world!")
$2 ==> true

jshell>  "ß".equalsIgnoreCase("ss")
$1 ==> false

jshell> "ß".equalsFoldCase("ss")
$2 ==> true

jshell> 

このほかにも、フランス語のŒとœなどがあります。ケースフォールディングの一覧は以下のリンクから。

CaseFoling.txt

 

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

BigIntegerクラスに演算メソッドが追加されています。

BigIntegerクラス

n乗根に関するメソッドが2つ追加されました。

  • BigInteger rootn(int)
  • BigInteger[] rootnAndRemainder(int)

rootn()メソッドはn乗根を求めるためのメソッドです。引数が2の場合はsqrt()メソッドと等価になります。

rootnAndRemainder()メソッドはn乗根と、自分自身とn乗根をn乗した数との差を配列で返すメソッドです。n乗根がrだとすると、rとthis - r**nが配列になります。こちらも、引数が2の場合はsqrtAndRemainder()メソッドと等価です。

 

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

クラスから列挙型に変更されるケースを初めて見たのですが、以前にもあったのでしょうか?

ByteOrder列挙型

ByteOrderはJava 25まではクラスだったのですが、列挙型に変更されました。これに伴い、定数の宣言が変更され、列挙型のメソッドが追加されています。

もともと、ByteOrderクラスの定数LITTLE_ENDIANとBIG_ENDIANの型はByteOrderクラスだったので、列挙型に変更してもプログラムを変更する必要はないはずです。

  • static ByteOrder valueOf(String)
  • static ByteOrder[] values()

 

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

時間間隔を表すDurationクラスに定数が追加されました。また、Instantクラスにもメソッドが追加されています。

 

Durationクラス

Durationクラスで保持できる最大時間間隔と最小時間間隔を表す定数が追加されています。なお、Durationクラスでは負の時間間隔も表せるので、最小となるのは負の値です。

  • Duration MAX
  • Duration MIN

MAXはLong.MAX_VALUEに999,999,999ナノ秒を加えた値で作成する時間間隔になります。MINの方はLong.MIN_VALUEで作成する時間間隔です。

 

Instantクラス

Instantクラスは時点を表すためのクラスです。これまで、自分自身の時点に指定された時間間隔を追加するメソッドとしてplusメソッドが使われてきました。これに対し、時間間隔を追加した結果が、InstantクラスのMAX、MINを超えてしまう場合、MAXとMINを返すplusSaturatingメソッドが追加されています。

  • Instant plusSaturation(Duration)

 

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

なぜ今さらという感じですが、Comparatorインタフェースにメソッドが2つ追加されました。

また、JEP 526 Lazy Constantに関連して、ListインタフェースとMapインタフェースにメソッドが追加されるのですが、これはpreviewなので、正式になった時に紹介します。

 

Comparatorインターフェス

2つの引数の大きい方/小さい方を返すmax()メソッド、min()メソッドが追加されました。

  • <U extends T> U max(U, U)
  • <U extends T> U min(U, U)

どういう時にこれらのメソッドを使うのか、イマイチ分からないんですよね。

 

java.desktop/java.awtパッケージ

AWTにメソッドが追加なんていつ以来でしょうか?とはいっても、GUIの機能を追加するのではなく、GUIのテストなどに使用するRobotクラスに簡易的なメソッドが追加されただけでした。

 

Robotクラス

Robotクラスは基本的な機能は追加されていないのですが、今までマウスのクリックでもmousePress()メソッドとmouseRelease()メソッドが分かれているなど、ちょっと使いにくい部分がありました。そこで、これらをもう少し分かりやすい形式でコールできるようになるためのメソッドが8種類追加されています。

  • void click()
  • void click(int)
  • void glide(int,int)
  • void glide(int,int,int,int)
  • void glide(int,int,int,int,int,int)
  • void type(int)
  • void type(char)
  • void waitForIdle(int)
  • int DEFAULT_DELAY
  • int DEFAULT_STEP_LENGTH

たとえば、click()メソッドは、mousePress()メソッド、waitForIdle()メソッド、mouseRelease()メソッド、waitFor()メソッドの4つのメソッドを順にコールしたものと同じ動作をします。

他のメソッドも、すでに存在するメソッドを組み合わせて簡単に呼べるようにしたというものです。

また、2つの定数は、たとえばclick()メソッドの内部でコールされるwaitForIdle()メソッドの初期値を表しているように、今回追加されたメソッドで使用する定数となっています。

 

java.management/javax.lang.managementパッケージ

MXBeanにメソッドが追加されました。

本来はMXBeanのインタフェースに抽象メソッドを追加したいところですが、後から追加ができないのでdefaultメソッド。default目祖度では意味のない値を返しているので、オーバーライド前提なのですが、本来であればインタフェースの変更したいところですね。

 

MemoryMXBeanインタフェース

GCのCPU時間を取得するためのメソッドが追加されました。

  • long getTotalGcCpuTime()

defaultメソッドでの実装は-1を返します。つまり、MemoryMXBeanインタフェースを実装するクラスが正しく実装しないと使えないメソッドになっています。まぁ、標準ライブラリなので、実装を忘れることはないでしょうけど。

 

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

JEP 517でHTTP/3がサポートされることになって、いろいろと変わっています。しかし、プロトコルが変わるだけなので、大きな使い方の変更はないです。

これについては、次のエントリーのJEPで語る方で紹介します。

 

java.sql/java.sqlパッケージ

JDBCのバージョンが4.3から4.5にアップしました。

一番の違いは、AutoCloseableに対応したことです。

 

Arrayインタフェース、Blobインタフェース、Clobインタフェース、SQLXMLインタフェース

これらの4つのインタフェースがすべてAutoCloseableインタフェースを実装するようになりました。これで、try-with-resources構文で使用することが可能です。

  • void close()

 

Connectionインタフェース

あまりJDBCを使うことがないので、よく分からないのですが、なぜか今ごろになってリテラルなどをクォーテーションするメソッドが追加されました。

  • String enquoteIdentifier(String,boolean)
  • String enquoteLiteral(String)
  • String enquoteNCharLiteral(String)
  • boolean isSimpleIdentifier(String)

 

JDBCType列挙型、Typesクラス

いずれも定数が2種類増えています。

  • DECFLOAT
  • JSON

 

まとめ

LTSの次のバージョンということで変更点は少ないものの、AWTやJDBCなど今まで変更の少なかったAPIに変更があったのは珍しいですね。

また、HTTP/3への変更もありますが、こちらは使い方はほとんど変わらないので、APIの変更も少なめです。これに関しては次のエントリーで紹介します。

 

さて、次のエントリーではJEPに関して簡単な説明を加えていく予定ですが... 今、さくらばは3月17日から開催されるJavaOneに参加するため、アメリカに滞在しています。そのため、JEPで語る方はJavaOneの後になりそうな予感が...

まぁ、のんびり待っていてください。

2025/12/25

レコードクラスの中身

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

本エントリーはJava Advent Calendar 2025の最終日です! 昨日は@zoosm3さんのDBFluteの複数DBをSpring Bootで実装する 開発のヒント でした。

 

24日に会社ブログでレコードクラスについて書きました。けっこうまじめに書いたので、ぜひ読んでみてください!

[Java] 今から始めるレコードクラス
https://bsblog.casareal.co.jp/archives/13817

レコードクラスの書き方や使い方については書きましたけど、じゃあレコードクラスって実際にはどういうバイトコードになっているの??というのが本エントリーです。

レコードクラスは簡単な記述で済んでいるのですが、コンパイルするとアクセッサーメソッドやequalsメソッドが自動生成されますとよく書かれていますが(会社ブログでもそう書きました)、実際のところどうなっているのというのを紹介していきます。

 

とりあえず、逆コンパイル

では、レコードクラスのバイトコードがどうなっているのか、さっそく調べてみましょう。題材にするのは、よく出てくる座標を表すPointレコードクラスです。

 

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

 

このクラスをコンパイルした後に、javapで逆コンパイルします。

 

> javac -g Point.java
> javap -p -v Point
Classfile /C:/temp/Point.class
  Last modified 2025/12/21; size 1348 bytes
  SHA-256 checksum 4bcbbc4b22a7b6c6fb72067dba973f9f08f80def796ecca1173c7c3b6596bda4
    Compiled from "Point.java"
public final class Point extends java.lang.Record
     ... 以下、略

 

javapのオプションの-pはprivateも逆コンパイルするというオプションで、-vは詳細情報を出力するオプションです。

この後にコンスタントプールが続くのですが、とりあえず重要なところで

 

public final class Point extends java.lang.Record

 

レコードクラスは継承ができないのですが、この行が理由です。

まず、java.lang.Recordクラスのサブクラスになるということ。そして、finalクラスだということです。

Recordクラスは以下の3種類の抽象メソッドを定義しています。

  • equals
  • hashCode
  • toString

もちろん、この3つのメソッドはObjectクラスで具象メソッドとして定義されているのですが、Recordクラスではそれらを抽象クラスとしてオーバーライドしています。

つまり、Ojbectクラスの実装は使わないということですね。

しかし、recordキーワードで定義されるレコードクラスは、これらのメソッドを定義する必要がありません。これらのメソッドがコンパイル時に自動生成されるというのが、このことからも分かります。

 

レコードコンポーネント

javapでは、クラス定義の後にコンスタントプールを出力します。コンスタントプールは必要に応じて参照すればよいので、その後に続く部分を見てみましょう。

通常のクラスであればフィールドの定義が続きます。レコードクラスではどうでしょう?

 

  private final double x;
    descriptor: D
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

  private final double y;
    descriptor: D
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

 

名前だけはレコードコンポーネントになりましたが、実質的にはインスタンスフィールドと同じでした。まぁ、想像通りですね。

イミュータブルなので、finalで宣言されているところが注目すべき点です。

 

コンストラクター

レコードクラスではカノニカルコンストラクターが自動的に生成されます。と言われても、カノニカルって何?という感じですよね。

カノニカル(Canonical)は「正規の」とか「標準的な」などの意味の単語です。このため、カノニカルコンストラクターを標準コンストラクターと記述しているドキュメントもあります。でも、標準と書かれるとStandardの方をイメージしてしまうんですよね。

ネイティブの人たちはCanonicalとStandardのニュアンスの違いを分かっているのでしょうが、私には理解できないのです...

 

それはそれとして、カノニカルコンストラクターはすべてのレコードコンポーネントを初期化するためのコンストラクターです。

では、Pointレコードクラスのカノニカルコンストラクターを見てみましょう。

 

  public Point(double, double);
    descriptor: (DD)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=5, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Record."<init>":()V
         4: aload_0
         5: dload_1
         6: putfield      #7                  // Field x:D
         9: aload_0
        10: dload_3
        11: putfield      #13                 // Field y:D
        14: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LPoint;
            0      15     1     x   D
            0      15     3     y   D
    MethodParameters:
      Name                           Flags
      x
      y

 

カノニカルコンストラクターの引数は、レコードクラスの定義におけるレコードコンポーネントの並びに対応しています。

Pointレコードクラスの場合、両方ともdoubleなので区別しにくいですが、第1引数がx、第2引数がyです。

 

一般的にコンストラクターのバイトコードでは、まずスーパークラスのデフォルトコンストラクターをコールします。それが0行目と1行目のinvokespecialです(行と書いていますが、実際にバイト数のことです。あしからず)。

4行目のaloadからがレコードコンポーネントの初期化になります。6行目のputfieldで引数のxの値をフィールドにセットしています。

同様に、11行目のputfieldで引数のyの値をフィールドにセットしています。

単純に引数の値をフィールド(レコードコンポーネント)に代入しているだけですね。

 

カノニカルコンストラクターは独自に定義することもできるのですが、それについては後述します。

 

アクセッサーメソッド

レコードクラスではレコードコンポーネントと同名のメソッドが生成され、レコードコンポーネントの値を取得できます。

まぁ、名前は違うもののgetterメソッドと同じですね。Pointレコードクラスのx()メソッドのバイトコードは以下のようになっていました。

 

  public double x();
    descriptor: ()D
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #7                  // Field x:D
         4: dreturn
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LPoint;

 

getfieldでフィールド(レコードコンポーネント)の値をスタックに積んで、dreturn (doubleのretuen)で返り値にしています。

y()メソッドも同様です。

 

他の自動生成されたメソッド

ここまでは、まぁ予想通りのバイトコードでした。

レコードクラスではカノニカルコンストラクターとアクセッサーメソッド以外に、次のメソッドを自動生成します。

  • equals
  • hashCode
  • toString

ここではequals()メソッドのバイトコードを見てみましょう。

equals()メソッドの書き方としては、Effective Javaが詳しいですね。Effective Javaの書き方と比べてどうなっているでしょう?

 

  public final boolean equals(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Z
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokedynamic #24,  0             // InvokeDynamic #0:equals:(LPoint;Ljava/lang/Object;)Z
         7: ireturn
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   LPoint;
            0       8     1     o   Ljava/lang/Object;

 

なんか全然違う!!

おもしろいのは、invokedynamic (indy)を使っているところです。つまり、equals()メソッドは初回実行時に動的に作られるということですね。

このindyの初回実行時にコールされるのが、ブートストラップと呼ばれるメソッドです。ブートストラップメソッドはクラスファイルの最後の方にあるBootstrapMethods:の箇所に記述されています。

 

BootstrapMethods:
  0: #49 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
    Method arguments:
      #8 Point
      #45 x;y
      #47 REF_getField Point.x:D
      #48 REF_getField Point.y:D

 

これを見ると、ブートストラップとして使われるのは、java.lang.runtime.ObjectMethodsクラスのbootstrap()メソッドだということです。実をいうと、equals()メソッドだけでなく、hashCode()メソッドもtoString()メソッドも同じブートストラップメソッドが使われています。

このObjectMethodsクラスはpublicなクラスなので、APIドキュメントが公開されています。Java 24であれば、以下のリンクから参照できます。

ObjectMethodsクラス
https://docs.oracle.com/javase/jp/24/docs/api/java.base/java/lang/runtime/ObjectMethods.html

ちょっとおもしろいのが、bootstrap()メソッドの最後の引数にアクセッサーメソッドのMethodHandleが使われているところですね。

 

では、ObjectMethodsクラスのbootstrap()メソッドを調べてみましょう。ソースは以下のリンクにあります。

ObjectMethods
https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/runtime/ObjectMethods.java

以下にObjectMethodsクラスのbootstrap()メソッドのソースを示します。

 

    public static Object bootstrap(MethodHandles.Lookup lookup, String methodName, TypeDescriptor type,
                                   Class<?> recordClass,
                                   String names,
                                   MethodHandle... getters) throws Throwable {

            <<省略>>
    
        List<MethodHandle> getterList = List.of(getters);
    
        MethodHandle handle = switch (methodName) {
            case "equals"   -> {
                if (methodType != null && !methodType.equals(MethodType.methodType(boolean.class, recordClass, Object.class)))
                    throw new IllegalArgumentException("Bad method type: " + methodType);

                yield makeEquals(lookup, recordClass, getterList);
            }

              <<省略>>
        };
 
        return methodType != null ? new ConstantCallSite(handle) : handle;
    }

 

メソッド名が"equals"であれば、makeEquals()メソッドをコールしています。

makeEquals()メソッドはClassfile APIでバイトコードを操作しています。ASMの頃に比べると、Classfile APIになって格段に読みやすくなりましたね。

ちょっと長いのですが、makeEquals()メソッドを以下に示しておきます。

 

    private static MethodHandle makeEquals(MethodHandles.Lookup lookup, Class<?> receiverClass,
                                           List<MethodHandle> getters) throws Throwable {
        MethodType rr = MethodType.methodType(boolean.class, receiverClass, receiverClass);
        MethodType ro = MethodType.methodType(boolean.class, receiverClass, Object.class);
        MethodHandle instanceFalse = MethodHandles.dropArguments(FALSE, 0, receiverClass, Object.class); // (RO)Z
        MethodHandle instanceTrue = MethodHandles.dropArguments(TRUE, 0, receiverClass, Object.class); // (RO)Z
        MethodHandle isSameObject = OBJECT_EQ.asType(ro); // (RO)Z
        MethodHandle isInstance = MethodHandles.dropArguments(CLASS_IS_INSTANCE.bindTo(receiverClass), 0, receiverClass); // (RO)Z
        MethodHandle accumulator = MethodHandles.dropArguments(TRUE, 0, receiverClass, receiverClass); // (RR)Z

        int size = getters.size();
        MethodHandle[] equalators = new MethodHandle[size];
        boolean hasPolymorphism = false;
        for (int i = 0; i < size; i++) {
            var getter = getters.get(i);
            var type = getter.type().returnType();
            if (isMonomorphic(type)) {
                equalators[i] = equalator(lookup, type);
            } else {
                hasPolymorphism = true;
            }
        }

        // Currently, hotspot does not support polymorphic inlining.
        // As a result, if we have a MethodHandle to Object.equals,
        // it does not enjoy separate profiles like individual invokevirtuals,
        // and we must spin bytecode to accomplish separate profiling.
        if (hasPolymorphism) {
            String[] names = new String[size];

            var classFileContext = ClassFile.of(ClassFile.ClassHierarchyResolverOption.of(ClassHierarchyResolver.ofClassLoading(lookup)));
            var bytes = classFileContext.build(ClassDesc.of(specializerClassName(lookup.lookupClass(), "Equalator")), clb -> {
                for (int i = 0; i < size; i++) {
                    if (equalators[i] == null) {
                        var name = "equalator".concat(Integer.toString(i));
                        names[i] = name;
                        var type = getters.get(i).type().returnType();
                        boolean isInterface = type.isInterface();
                        var typeDesc = type.describeConstable().orElseThrow();
                        clb.withMethodBody(name, MethodTypeDesc.of(CD_boolean, typeDesc, typeDesc), ACC_STATIC, cob -> {
                            var nonNullPath = cob.newLabel();
                            var fail = cob.newLabel();
                            cob.aload(0)
                               .ifnonnull(nonNullPath)
                               .aload(1)
                               .ifnonnull(fail)
                               .iconst_1() // arg0 null, arg1 null
                               .ireturn()
                               .labelBinding(fail)
                               .iconst_0() // arg0 null, arg1 non-null
                               .ireturn()
                               .labelBinding(nonNullPath)
                               .aload(0) // arg0.equals(arg1) - bytecode subject to customized profiling
                               .aload(1)
                               .invoke(isInterface ? Opcode.INVOKEINTERFACE : Opcode.INVOKEVIRTUAL, typeDesc, "equals", MTD_OBJECT_BOOLEAN, isInterface)
                               .ireturn();
                        });
                    }
                }
            });

            var specializerLookup = lookup.defineHiddenClass(bytes, true, MethodHandles.Lookup.ClassOption.STRONG);

            for (int i = 0; i < size; i++) {
                if (equalators[i] == null) {
                    var type = getters.get(i).type().returnType();
                    equalators[i] = specializerLookup.findStatic(specializerLookup.lookupClass(), names[i], MethodType.methodType(boolean.class, type, type));
                }
            }
        }

        for (int i = 0; i < size; i++) {
            var getter = getters.get(i);
            MethodHandle equalator = equalators[i]; // (TT)Z
            MethodHandle thisFieldEqual = MethodHandles.filterArguments(equalator, 0, getter, getter); // (RR)Z
            accumulator = MethodHandles.guardWithTest(thisFieldEqual, accumulator, instanceFalse.asType(rr));
        }

        return MethodHandles.guardWithTest(isSameObject,
                                           instanceTrue,
                                           MethodHandles.guardWithTest(isInstance, accumulator.asType(ro), instanceFalse));
    }

 

簡単にいうと、まずレコードコンポーネントごとにその型に応じたequals()メソッドを探します。次に、レコードコンポーネントごとにequals()メソッドをコールするメソッドを持つクラスを動的に作成して、そのメソッドのMethodHandleを作っています。

なかなかおもしろいですね。

 

Classfile APIのビルド系のメソッドはバイトコードと対応したメソッド名になっているので、バイトコードを読めればだいたい分かるはずです。

 

というわけで、equals()メソッド、hashCode()メソッド、toString()メソッドは中身が動的に作成されるのでした。

 

カノニカルコンストラクター再び

最後に、もう一度カノニカルコンストラクターについて。

前述したカノニカルコンストラクターは自動生成されたものでしたが、カノニカルコンストラクターは自分で書くこともできます。

たとえば、範囲を示すRangeレコードクラスで下限が上限を超える場合IllegalArgumentException例外をスローするようなカノニカルコンストラクターを記述してみます。

 

public record Range(double min, double max) {
    public Range {
        if (min > max) {
            throw new IllegalArgumentException();
        }
    }
}

 

カノニカルコンストラクターは引数がレコードコンポーネントの宣言部分と同じなので省略した書き方になります。

そして、コンパイルした後のバイトコードが以下になります。

 

  public Range(double, double);
    descriptor: (DD)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=4, locals=5, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Record."<init>":()V
         4: dload_1
         5: dload_3
         6: dcmpl
         7: ifle          18
        10: new           #7                  // class java/lang/IllegalArgumentException
        13: dup
        14: invokespecial #9                  // Method java/lang/IllegalArgumentException."<init>":()V
        17: athrow
        18: aload_0
        19: dload_1
        20: putfield      #10                 // Field min:D
        23: aload_0
        24: dload_3
        25: putfield      #16                 // Field max:D
        28: return
      LineNumberTable:
        line 2: 0
        line 3: 4
        line 4: 10
        line 2: 18
        line 6: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   LRange;
            0      29     1   min   D
            0      29     3   max   D
      StackMapTable: number_of_entries = 1
        frame_type = 255 /* full_frame */
          offset_delta = 18
          locals = [ class Range, double, double ]
          stack = []
    MethodParameters:
      Name                           Flags
      min                            mandated
      max                            mandated

 

前半にminとmaxの比較を行って、minが大きければ例外をスローする処理が記述されています。18行目からコンストラクターの引数をフィールド(レコードコンポーネント)に代入する処理です。

フィールドに代入する処理から行われると思っていたら、それは最後なんですね。ちょっと意外でした。

 

まとめ

レコードクラスはデータを扱うのに便利なクラスですが、その中身がどうなっているのかを紹介しました。

 

まとめてみると

  • レコードクラスはRecordクラスのサブクラスでfinalクラス
  • レコードコンポーネントはfinalなインスタンスフィールド
  • カノニカルコンストラクターが生成される
  • アクセッサーメソッドはgetter相当
  • equals(), hashCode(), toString()は実行時に動的に作成
  • カノニカルコンストラクターでフィールドへの代入は最後

 

レコードクラス自体は単純ですが、その裏側はなかなかおもしろかったですね。

OpenJDKのソースを読むのはたいへんですが、このぐらいの小さいところから読み始めるというのはいいかもしれません。