2026/09/24

JEPで語るJava 27

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

JEPで語る方もおなじみになってきました。

Java 27のJEPは全部で9つ。そのうち、Standard JEPは4つで、Preview JEPとIncubator JEPは5つです。

Java 27のStandard JEPの一覧はこちら。

  • JEP 523: Make G1 the Default Garbage Collector in All Environments
  • JEP 527: Post-Quantum Hybrid Key Exchange for TLS 1.3
  • JEP 534: Compact Object Headers by Default
  • JEP 536: JFR In-Process Data Redaction

Standard JEPのうちAPIの変更されるのはJEP 527ですが、既存のコードのままでも恩恵を受けることができます。

Preview JEPとIncubator JEPの一覧はこちら。

  • JEP 531: Lazy Constants (Third Preview)
  • JEP 532: Primitive Types in Patterns, instanceof, and switch (Fifth Preview)
  • JEP 533: Structured Concurrency (Seventh Preview)
  • JEP 537: Vector API (Twelfth Incubator)
  • JEP 538: PEM Encodings of Cryptographic Objects (Third Preview)

 

いつもと同じように、PreviewやIncubatorは軽く触れるだけで、主にStandard JEPについて紹介していきます。

ただし、JEP 527とJEP 538はセキュリティ関連なので省略します。

 

JEP 523: Make G1 the Default Garbage Collector in All Environments

サーバー環境でG1GCがデフォルトのGCになったのがJava 9。

STWを減らすために、アプリケーションスレッドと同時にGCスレッドも動作するので、本来はコアがたくさんあるような環境が適しているのは明らかです。また、ヒープをリージョンに分割するのも、ヒープがある程度余裕がないとリージョンを効果的に使えません。

したがって、リソースに余裕があるサーバー環境でG1GCがデフォルトGCというのは理にかなっています。

しかし、G1GCは着実に改良され続けて、低リソース環境でもシリアルGCと同等のスループットを得ることができるようになってきました。これってなにげにすごいことだと思うんですよね。

ということで、今まで低リソース環境ではシリアルGCがデフォルトGCだったのを、Java 27からG1GCがデフォルトGCになりました。

とはいうものの、本当にシリアルGCと同じスループットが出るかどうかはアプリケーションごとに検証が必要かもしれません。

低リソース環境といえばコンテナで動かしている場合ですね。今までシリアルGCを使用していたのであれば、G1GCを評価しておいた方がよいかもしれません。

ただ、Java 27はLTSではないので、実際に使われるのはLTSのJava 29でしょう。Java 29がリリースされるまで時間的に余裕があるので、急いで評価する必要はないかもしれないですね。

 

JEP 534: Compact Object Headers by Default

JEP 534もJEP 523と同様にデフォルト動作の変更です。

何が変更になったのかというと、オブジェクトヘッダーです。

Project Lilliputで進めていたオブジェクトヘッダーのコンパクト化はJava 25で導入されました(JEP 519)。しかし、この時はコンパクトオブジェクトヘッダーを試すには、Javaのオプションで -XX:+UseCompactObjectHeaders を指定する必要がありました。

そして、コンパクトオブジェクトヘッダーが導入されて1年、とうとうコンパクトオブジェクトヘッダーがデフォルトになったのでした。

逆に以前のオブジェクトヘッダーを使用するには、Javaのオプションで -XX:-UseCompactObjectHeaders を指定します。

 

JEP 536: JFR In-Process Data Redaction

Redactionは編集という意味なのですが、閲覧できないように黒塗りにすることもRedactionなのだそうです。

何を黒塗りするかというと、Javaの起動時オプションで指定した機密情報です。

JDK Flight Recorder (JFR)はアプリケーションの診断やプロファイリングに使う情報を収集する便利なフレームワークです。

JFRではいろいろな情報を収集するのですが、その中には起動時オプションも含まれます。

しかし、起動時オプションには公になっていないURIや、パスワードなどを指定することもあります。このため、JFRの記録ファイルから情報漏洩の可能性が発生してしまいます。

自分では解析できないので、第三者に記録ファイルを渡して解析してもらうようなケースですね。

そこで、機密情報に当たるような起動時オプションは記録しないようにしましょうというのが、JEP 536です。

これ、自分で黒塗りにするところを指定するのだと思っていたら、デフォルトのままでも結構黒塗りにしてくれます。たとえば、次のようにTestクラスを実行したとします。

> java -XX:StartFlightRecording:filename=dump.jfr Test -password abcde

Testクラスの引数として-passwordでパスワードを指定するようなパターンです。

Testクラスの実行後、JDK Mission Control (JMC)でdump.jfrファイルを読み込んでみましょう。下図はJMCのシステムプロパティの一覧です。

sun.java.commandのところを見てみると"Test [REDACTED] [REDACTED]"になっています。つまり、この引数がパスワードであると判別して記録しないようになっているわけです。

しかし、passwordではなく、psswrdやpassだとRedactしないようです。

そこで、デフォルトでRedactしない情報については、個別に指定する必要が出てきます。

Redactする情報は -XX:FlightRecorderOptions オプションで指定します。

-XX:FlightRecorderOptions オプションの後に、以下の2種類のサブオプションで情報を指定します。

  • redact-aargument: コマンドライン引数
  • redact-key: 環境変数もしくはシステムプロパティ

たとえば、デフォルトではRedactされなかった-psswrdとその後の情報をRedactするには以下のように指定します。

> java -XX:StartFlightRecording:filename=dump.jfr -XX:FlightRecorderOptions:'reduct-argument=-psswrd *' Test -psswrd abcde

*は任意の文字列にマッチするワイルドカードです。1文字にマッチする?も使用できます。

このように指定することで、JFRの記録ファイルからの機密情報漏洩を防ぐことができます。

 

Preview JEP/Incubator JEP

お試し機能のJEPも簡単に了解しましょう。

 

JEP 531: Lazy Constants (Third Preview)

JEP 531は3回目のプレビューですが、Lazy Constantsになってからは2回目。

Lazy Constantsは、初期化に時間のかかるオブジェクトを遅延初期化させるためのAPIです。

Java 27での変更は少しだけ。

isInitializedメソッドとorElseメソッドの削除と、Set.ofLazyメソッドの追加です。

もともとそれほど大きくないAPIですし、変更も少しなので、そろそろStandard JEPになりそうな感じ。

Java 28で変更なしでもう一度Preview、Java 29でStandard JEPというのが櫻庭の予想ですw

 

JEP 532: Primitive Types in Patterns, instanceof, and switch (Fifth Preview)

パターンマッチングにプリミティブ型を使えるようにしようというのがJEP 532。

Java 27では変更なしでもう一度Previewなので、Java 28でStandard JEPになるのでしょう。

 

JEP 532についてはカサレアルのブログに詳しく書いたので、そちらをご参照ください!

 

switchのパターンマッチングにプリミティブ型を使う
https://bsblog.casareal.co.jp/archives/14973

 

JEP 533: Structured Concurrency (Seventh Preview)

複数のスレッドで実行した結果をまとめるような用途に使用するのがStructured Concurrencyです。

Structured ConcurrencyはProject Loomで仕様策定されているので、もともとはVirtual Threadと一緒に出せればよかったのですが、なんと今回7回目のプレビュー。

しかし、Java 28でStandard JEPになることがほぼ決まりました!

 

とはいうものの、Java 27でも変更点があります。

  • StructuredTaskScopeインタフェースとStructuredTaskInterface.Joinerインタフェースにjoin()メソッドなどでスローさrふぇる例外の型を表す型パラメータが追加
  • StructuredTaskScopeインタフェースに、引数にStructuredTaskScope.Configurationインタフェースをとるopen()メソッドのオーバーロードが追加
  • allSuccessfulOrThrow()メソッドなどのファクトリーメソッドで生成されるStructuredTaskInterface.Joinerオブジェクトが、joinメソッドでExecutionException例外をスローするように変更
  • StructuredTaskScopeインタフェースのawaitAll()メソッドが削除
  • StructuredTaskScope.JoinerインタフェースのonTimeout()メソッドがtimeout()メソッドに名称変更

 

JEP 537: Vector API (Twelfth Incubator)

Value Classが正式にサポートされるまでIncubatorが続くVector APIです。

Java 27でも変更なしでIncubatorが続きます。

このまま永遠にIncubatorのままなのではないかと思われたのですが、ここに来て動きが!というのも、Java 28でValue ClassがPreview JEP (JEP 401)になります!

すぐにStandard JEPになることは考えられないですけど、次の次のLTSには間に合うかなぁ....???

そうしたら、やっとVector APIもStandard JEPへ!

 

まとめ

Java 27では、JEPで語れない方のAPI変更は少なかったですが、JEPの変更も少な目。

目新しいものはありませんし、Standard JEPになったのもデフォルト動作の変更なので。

まぁ、毎回派手に新機能が入るわけではないので、しかたありません。

しかし、次のJava 28では、いろいろと変化がありそうです。Value Classだけでなく、新しいJSON APIやLeydenの新しいJEPも含まれています。

楽しみに待つことにしましょう!

2026/09/15

JEPでは語れないJava 27

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

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

Java 27はLTSの間の中間のバージョンからか導入された機能も裏方的なものが多く。APIの変更も少ししかありません。まぁ、そんなもんです。

Java 27のStandard JEPは4つ。

  • JEP 523: Make G1 the Default Garbage Collector in All Environments
  • JEP 527: Post-Quantum Hybrid Key Exchange for TLS 1.3
  • JEP 534: Compact Object Headers by Default
  • JEP 536: JFR In-Process Data Redaction

Preview JEPとIncubator JEPは5つ。

  • JEP 531: Lazy Constants (Third Preview)
  • JEP 532: Primitive Types in Patterns, instanceof, and switch (Fifth Preview)
  • JEP 533: Structured Concurrency (Seventh Preview)
  • JEP 537: Vector API (Twelfth Incubator)
  • JEP 538: PEM Encodings of Cryptographic Objects (Third Preview)

Java 26のJEPが10でしたから、さらに減っています。

Standard JEPでは、JEP 523とJEP 534がヒープに関するもの。どちらもデフォルト動作が変更されるというものです。

JEP 527とJEP 536はセキュリティ関連ですね。

 

Preview JEPとIncubator JEPは新規はなし。一番少ないものでも3回目のPreviewなので、新規性はないです。

JEPについての詳細は別エントリーで。

 

さて、JEPで語れない方です。Java 27はAPIの変更も少なめ。ほとんどがjava.baseモジュールなので、今回はjava.baseモジュールだけです。

なお、今回もセキュリティ関連や、バージョンに関する定数については省略です。

 

廃止になったAPI

Java 27で削除されたAPIは、メソッドが1つだけです。

メソッド

スレッドプールを提供するExecutorServiceインタフェースのコンクリートクラスであるThreadPoolExecutorクラスのメソッドが1つだけ削除されました。

  • java.util.concurrent.ThreadPoolExecutor.finalize()

基本的にfinalize()メソッドは使わないようにすべきです。ThreadPoolExecutorクラスが実装しているExecutorServiceインタフェースがAutoClosableインタフェースのサブインタフェースになっているため、なおさらfinalize()メソッドは必要ないですね。

もし、finalize()メソッドを使用しているようであれば、try-with-resources構文を使用するようにしましょう。

 

 

廃止予定に追加されたAPI

Java 27で、forRemoval=trueになったAPIはありません。

 

追加されたAPI

Java 27ではStandard JEPでのAPI変更がないので、全体的にAPIの変更も少ないです。

 

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

数値演算のメソッドが追加されているのがメインですね。

 

Math/StrickMathクラス

逆双曲線関数関連のメソッドが3つ追加されました。

  • double acosh(double x)
  • double asinh(double x)
  • double atanh(double x)

メソッド名の通り逆双曲線余弦関数、逆双曲線正弦関数、逆双曲線正接関数を求めるためのメソッドです。

 

Stringクラス

文字列をエンコードした時の文字列サイズを求めるメソッドが追加されました。

  • int encodedLength(Charset cs)

Javaは内部文字セットとしてUTF-16を使用しているので、String.length()メソッドではUTF-16のサイズを返します(文字列長ではないです)。これに対し、指定した文字セットでエンコードしたバイト列のバイト長を返すのがencodedLength()メソッドです。

 

jshell>  var text = "こんにちは"
text ==> "こんにちは"

jshell> text.length()
$2 ==> 5

jshell> text.encodedLength(Charset.forName("UTF-8"))
$3 ==> 15

jshell> text.encodedLength(Charset.forName("Shift_JIS"))
$4 ==> 10

jshell> 

内部的にはgetBytes(cs).lengthをコールしているのと同等になります。

 

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

Classfile APIのオペコードに対応するインタフェースにメソッドが追加されました。

 

IncrementInstructionインタフェース

IncrementInstructionインタフェースは、オペコードのiincとiinc_wに対応するインタフェースです。

これまでもファクトリーメソッドであるof()メソッドが提供されていましたが、of()メソッドがオーバーロードされました。

  • static IncrementInstruction of(Opcode op, int slot, int constant)

これまでのof()メソッドはオペコードを指定することができませんでしたが、オーバーロードされたof()メソッドはオペコードが指定できます。

指定できるオペコードはOpcode.IINCもしくはOpcode.IINC__Wです。

 

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

FFMのメモリに関するインタフェースにメソッドが追加されました。

 

MemorySegmentインタフェース

FFMでメモリ領域に対応するMemorySegmentインタフェースにメソッドが2つ追加されました。

どちらも既存のメソッドのオーバーロードになっています。

 

  • static long copy(String src, Charset dstEncoding, int srcIndex, MemorySegment dst, long dstOffset, int numChars)
  • String getString(long offset, Charset charset, long byteLength)

既存のcopy()メソッドは、MemorySegmentからMemorySegmentへのコピーでした。これに対し、新しいcopy()メソッドは文字列をMmoerySegmentにコピーします。

一方のgetString()メソッドはバイト長を指定できるようになりました。

 

SegmentAllocatorインタフェース

メモリーのアロケートを行ってMemorySegmentオブジェクトを返すのがSegmentAllocatorインタフェースです。

こちらも、メソッドのオーバーロードが追加されました。

 

  • MemorySegment allocateFrom(String str, Charset charset, int srcIndex, int numChars)

ソースとなる文字列の開始インデックスと文字数を指定できるようになりました。

 

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

BigDecimalクラスにメソッドが1つ追加されました。

 

BigDecimalクラス

意外にもn乗根を求めるメソッドが今までなかったのでした。BigIntegerクラスはJava 26でn乗根を求めるメソッドが追加されたので、それに続く変更になります。

 

  • BigDecimal rootn(int n, MathContext mc)

第2引数のMthContextクラスは精度を指定するためのクラスです。

 

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

Date & Time APIのフォーマッターに関するクラスが新規で導入されました。

 

DateTimeFormatterPatternProviderクラス

DateTimeFormatterPatternProviderクラスロケールごとに日時フォーマットを文字列を作成するためのクラスで、LocaleServiceProviderクラスのサブクラスです。

DateTimeFormatterPatternProviderクラスも抽象クラスであり、アプリケーションごとに以下のメソッドをオーバーライドします。

 

  • String getDateTimeFormatterPattern(String requestedTemplate, String calType, Locale locale)
  • String getDateTimeFormatterPattern(FormatStyle dateStyle, FormatStyle timeStyle, String calType, Locale locale)

 

その他の変更

Java 27というわけではありませんが、2026年8月からOracleがセキュリティアップデートの変更を行いました。

今までのセキュリティアップデートはCPU (Critical Patch Update)といい、1月、4月、7月、10月に行われてきました。

これに対し、新たにCSPU (Critical Security Patch Update)が追加され、2月、3月、5月、6月、8月、9月、11月、12月に行われます。

CPUとCSPUを合わせると、毎月セキュリティアップデートがあることになります。

すでに、2026年8月にはCSPUに対応するJava 26.0.2.1がリリースされています。今までのCPUが3つ目の数字で、CSPUが4つ目の数字ということになったようです。

これはOracle JDKの話ですが、結局は他のディストリビューターも追従するようですね。

 

参考: 月次のCritical Security Patch Updates (CSPUs)を2026年5月28日より開始

 

 

まとめ

LTSの間の中間のバージョンだからなのか、変更点がとても少ないです。

JEPによる変更も少ないのですが、これに関しては次のエントリーで紹介します。

 

次のJava 28は、やっとValue ClassがPreviewになります。これ以外にもプリミティブ型のパターンマッチングなどがStandard JEPになりそうなので、いろいろ動きがありそうです。

Java 27は嵐の前の静けさなのかもしれません。

2026/08/19

探偵!ナイトスクープ Java Ring回の裏側

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

ひょんなことから探偵!ナイトスクープに出演することなっていました。人生長く生きていると、何が起こるか分からないものです。

 

どうして出演することになったのかとか、他の出演者がどうやって決まったのかなどの話はともかく、Java Ringですよ。

実際の撮影は1日がかりだったのですが、放送はたった10分。削られた部分が多くあるので、その部分を補っていくというのが、このエントリーです。

 

依頼者の山﨑さんも顛末をブログに書いているので、そちらもぜひご覧になってください。

 

番組はYouTubeで見られます。

 

Java Ring

Java Ringは1998年にサンフランシスコで開催されたJavaOne 98で、Sun Microsystemsが参加者に配布したガジェットです。その後、日本など世界各地のJavaのカンファレンスやイベントで参加者に配布されています。

櫻庭はJavaOne 98でもらったもの以外に、日本の2つのイベントでも取得したので3つあったのですが、1つは行方不明😰

イベントで配布されたものはプロトタイプで、その後実際に販売されたようですが、製品版は見たことがないです。

 

Java Card

さて、この当時、Javaに複数のエディションを作成すると発表されていました。Java SE (Standard Edition)、Java EE (Enterprise Edition)、Java ME (Micro Edition)の3種類です。今もOpenJDKで開発が進められているのが、Java SEですね(SEとは言わなくなってしまいましたが...)。そして、Jakarta EEがJava EEの後継です。

残念ながら組み込み向けのJava MEはなくなってしまいましたが、Java MEよりもさらにリソースが限られたデバイス向けのJavaがあります。

それがJava Cardです。

Cardという名前であることから分かるとは思いますが、クレジットカードがメインのターゲットです。その他にもSIMカードなどにJava Cardは使われていました。

クレジットカードを見れば分かる通り、バッテリーはありません。つまり、カードを親機に接続したときに供給される電力だけで動作します。

また、計算リソースも極端に限定されており、浮動小数点数はおろか、整数でも4バイト幅のintは使用できません。

このため、Javaといっても、Java SEとはかなり違うものです。

 

Java CardではCardletというアプリケーションが動作します。このCardletのクラス名がAppletなのが誤解を生んでいました。

Java CardのAppletはjavacard.framework.Appletクラスで、いわゆるアプレットはjava.applet.Appletクラス。できることも違えば、メソッドも異なりますし、動作も全然違います。

Cardletでは、ほんとに限定的なことしかできないのです。まぁ、クレジットカード上で動作すると考えれば妥当ですね。

 

Java Ringとは

さて、Java Ringです。

Java Ringは、Java Cardに準拠したデバイスです。

もともとはiButtonというボタン型のデバイスがありました。そこにJava CardのVMであるCardVMをのせ、指輪状に加工したのがJava Ringです。

通常のJava Cardとは違って、バッテリーが内蔵されており、メモリもクレジットカードに比較すれば多く使用できました。このため、複数のCardletを使用できるようになっています。ただし、複数のCardletを同時に動かすことはできません。

1998年のJavaOneでは、山﨑さんがナイトスクープで言及してたコーヒーの好みを記憶させておくCardletや、マンデルブロー集合の各ピクセルを計算させてJavaOne参加者でマンデルブロー集合の図を表示させるCardletが使用できるようになっていました。とはいえ、コーヒーの好みを記憶させておくだけで、Java Ringを接続するとコーヒーを淹れてくれるというのはなかったんですけどね。

 

このようなJava Ringですが、バッテリーを搭載しているために、逆にバッテリーがなくなってしまうと動作しなくなってしまいます。そこで、山﨑さんがバッテリーを交換したいとナイトスクープに駆け込んだわけです。

 

Java Ringの構造

ソフトウェアの構造でもハードウェアでもなく、物理的な構造です。

Java Ringは、iButtonを指輪状に加工したものです。ベースになるのはiButtonが収まるサイズの穴がある指輪です。

その穴に樹脂を流し込んで、その後にiButtonを埋め込み、さらに隙間に樹脂を流し込んで、iButtonを固定してあります。

Java Ringの穴の裏側には空気抜きのための小さな穴が開いています。

_DSC5217

iButtonは筐体全体が接点になっており、側面および底面がグランド、トップの部分が信号ラインとなっています。これをアダプターに接続して親機と通信できるようになっています(この通信方法は1-Wireと呼ばれています)。

このため、Java Ringでもアダプターに接続できるように、トップの部分が露出した形状になっています。

_DSC5233

 

さて、このような構造のJava Ringですが、バッテリーを交換するにはiButtonを開封しなければいけません。

ナイトスクープではJavaの専門家たちに集まっていただきましたが、さすがにソフト屋には手も足も出ません。

そこで、登場していただいたのがプロモデラーのまつおーじさんです。

今回、Java Ringが復活できたのも、ほんとにまつおーじさんのおかげです。

まつおーじさんが行ったのが、iButtonの周りの樹脂の部分をタガネで掘っていくというものです。下の図にJava Ringの輪切りにした図ですが、この黒い樹脂の部分を削っていったわけです。

番組ではあっという間に掘り終わったように見えますが、実際にはこれだけで2時間近くかかっています。

上から金属が見えるところまで掘り進み、その後裏側の小さい穴も掘り進めていきます。これで下図のような状態になりました。

この後、小さい万力を利用して、下の穴から力をかけて押していきました(上手の矢印の部分)。この結果、iButtonがスポッと取り出せたのでした。

これも番組では簡単に取り出せたように見えますけど、これもなかなか引っかかりがなくて時間がかかっています。

次は、取り出したiButtonを切断してバッテリーを取り出すわけですが、これもそんなに簡単にできるものではありません。

もともとiButtonはバッテリーを交換するようにはできていません。バッテリーを取り出すには、何らかの方法でiButtonを切断しなくてはなりません。

山﨑さんも紹介しているiButtonの電池交換レポートでは、グラインダーを使って切り進んでいますが、これは中の基盤などを壊してしまう可能性も高いです。

iButtonの形状を図式化したのが下図です。

ここで、まつおーじさんが注目したのが、iButtonの底部にあるフリンジです。下の図はiButtonを水平に見た時の図ですが、フリンジは赤丸の部分ですね。

このフリンジは、iButtonを他のデバイスなどに装着しやすくするための出っ張りです。ですが、iButton自体をかしめるにもこの形状が都合がよかったのだと思います。

このフリンジの部分をペンチで繰り返し曲げ伸ばしすることで、金属疲労によりちぎりとっていったわけです。

最終的には缶詰を開けた時のように、一周ぐるりとフリンジを取り除き、底部をパカッと開けることができたのでした。

そして、バッテリーは底部にあったので、無事に交換ができたのでした!!

ちなみに、下の写真はバッテリーを抜いた後です。

と、ここまでが物理的な話。番組だとかなりはしょられてはいましたけど、こちらがメインでしたね。

まぁ、ソフトは何かやっていても絵にならないからしかたないです。

 

Java Ringのソフトウェア構成

さて、ここからがソフトウェアの話です。

Java Ringのソフトウェアの構成を示したのが下の図です。

ベースの部分にファームウェアがあり、その上にJava CardのVMであるCardVMが動作しています。リソースが限られているため、OSに相当する部分はありません。

また、ファームはバッテリーがなくても、1-Wireからの給電で最低限の動作は可能です。

山﨑さんのblogエントリーの 【Java Ring】05. CRC エラー! で、バッテリーがなくても通信ができているのはこのためです。しかし、バッテリーが切れてしまったため、CardVMは動作しません。このため、CRCエラーが発生してしまっています。

そして、CardVM上でCardletが動作するという構成になっています。

また、Master PINなどの秘密情報を保持している領域も用意されています。

 

このような構成になっているわけですが、当然のことながらそれぞれの構成要素がメモリーのどこかに保持されています。

問題はどこまでがバッテリーがなくてもメモリー内容を保持できるかどうかということです。

メモリーには電源が供給されないと内容を保持できない揮発性メモリーと、バッテリーがなくても大丈夫な不揮発性メモリーがあります。

不揮発性メモリーにも、長期的に安定しているものと、長期的には内容を保持できないものがあります。

前者にはEPROM、後者にはフラッシュメモリーなどがあります。前者は一般的にROM、後者はNVRAMと呼ばれることがあります。

iButtonでも、EPROMとNVRAMが使われています。

問題はどこまでがROMに保存されているかということです。

もう1つ考えなくてはならないのが、耐タンパー性です。

耐タンパー性とは外部からの攻撃を受けた時の耐性を示しています。攻撃に対する対応にはいろいろありますが、漏洩してはならない情報を消去するという手法もあります。

Java Ringはパーソナルなガジェットなので、分解しようとすると暗号鍵などの秘密情報は耐タンパー性により消去されてしまうのではないかということです。

事前に大山さん、石原さんと話していたのは秘密情報は、まず消去されるでしょうということ。また、Cardletもバッテリーがなくなれば保持できないであろうということです。

で、議論になったのが、CardVMです。

製品版のJava Ringならともかく、プロトタイプでイベント用に配布するために作られたJava RingでCardVMをわざわざROMに焼くかということです。

3人の意見は、多分ダメだろうと。

ところが、バッテリーを交換してビックリ、CardVMが生きていたのでした!!

 

とはいえ、秘密情報は消去されてしまったので、初期化されてしまった状態です。収録の時には、Cardletをロードするところでエラーになってしまいました。事前に工場出荷時のMaster PINの候補はいくつか探していたのですが、いずれも失敗。

結局、残念ながら時間切れ。

その後、山﨑さんが自宅で設定をいろいろと見直すことでCardletのロードに成功したのでした。どうやら、工場出荷時にはMaster PINが設定されていなかったようです。

その後、Cardletの実行までできたのは彼のblogにある通りです。

 

ということで、Java Ringが復活したのでした!!

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のソースを読むのはたいへんですが、このぐらいの小さいところから読み始めるというのはいいかもしれません。

2025/12/06

Lazy Constant 実装編

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

本エントリーはJava Advent Calendar 2025の6日目です。

 

一昨日はLazy Constantの使い方を紹介したので、今日はLazy Constantがどうやって実装されているかを紹介していきます。

 

Lazy Constant
https://www.javainthebox.com/2025/12/lazy-constant.html

 

シングルトン

いきなりシングルトンと言われても... という感じだとは思いますが、Lazy Constantの実装を紐解く前に、シングルトンについて考えてみます。

シングルトンはデザインパターンの1つで、インスタンスを1つに制限するためのパターンです。

シングルトンは一種の大域変数になってしまうので、使いすぎるのはよくないですし、最近はほとんど使われなくなったように思います。でも、その実装はLazy Constantにつながるのです。

そもそもシングルトンの実装ってどうなっていたか覚えていらっしゃいますでしょうか?

 

シンプルなシングルトン

まずはシンプルなシングルトンの実装です。

シングルトンは自身のインスタンスをstaticフィールドで1つ保持します。そして、インスタンスを取得するメソッドが初めてコールされた時に、インスタンスを生成します。インスタンスが存在すれば、それを返します。つまり、遅延初期化なのです。

 

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton get() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

 

インスタンスを勝手に作られないように、コンストラクターはprivateで宣言します。

getメソッド(getInstanceメソッドのことも多いですが、ここではgetメソッドにします)では、staticフィールドのinstanceがnullだったら、つまり初期化されていなければSingletonインスタンスを生成します。そして、そのinstanceを返します。

シングルトンにデータを持たせるのであれば、それなりにフィールドなどを定義しますが、インスタンスを1つに限定させるための実装としてはこれでOKです。

しかも、実際にシングルトンのインスタンスを実際に使う時まで、その初期化を遅らせることができます。

しかし、問題もあります。この実装はスレッドセーフではないという点です。シングルスレッドであればいいのですが、マルチスレッドでは使えません。

 

スレッドセーフなシングルトン

スレッドセーフにするにはどうすればよいでしょう?

もっとも単純なのは、getメソッドをsynchronizedにするという方法です。

 

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public synchronized static Singleton get() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

 

これでマルチスレッドでも動作します。

問題はスケールしないということです。スレッド数が2, 3であればよいのですが、スレッド数が増えるととたんにロック待ちで渋滞してしまいます。

instanceフィールドは初期化した後は参照されるだけなので、本来であれば同期化は必要ありません。問題は未初期化の状態時に複数スレッドからgetメソッドをコールされる場合です。

 

そこで、getメソッドのインスタンス生成のところだけ同期化することを考えてみます。

 

    public static Singleton get() {
        if (instance == null) {
            // NG これはダメな実装
            synchronized(instance) {
                instance = new Singleton();
            }
        }

        return instance;
    }
}

 

しかし、これではダメなのです。

CPUの効率化のために命令を入れ替えたり、キャッシュにinstanceの値が残っていることもあるので、instanceを初期化しているときに複数のスレッドからアクセスされてしまうことがあります。

そこで、出てくるのがダブルチェックロックという方法です。でも、単にダブルチェックロックだけでは、キャッシュ上のinstanceとメモリのinstanceが同期化されているか保証されません。

そこで、instanceフィールドをアトミックに処理されるようにします。

厳密にやるのであればjava.util.concurrent.atomic.AtomicReferenceクラスを使用しますが、volatileでも大丈夫です。

最終的には次のようになります。

 

public class Singleton {
    // volatileで宣言することによりアトミック性を保証する
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton get() {
        if (instance == null) {
            synchronized (Singleton.class) {
                // ダブルチェック
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}

 

これでマルチスレッドでスケールするスレッドセーフなシングルトンになります。

 

Lazy Constant

シングルトンが理解できたとして、Lazy Constantの実装です。

java.lang.LazyConstantはインタフェースなので、実際の遅延初期化の部分は実装されていません。どこで実装されているかというと、LazyConstantインタフェースのstaticメソッドのofメソッドを見れば分かります。

 

    static <T> LazyConstant<T> of(Supplier<? extends T> computingFunction) {
        Objects.requireNonNull(computingFunction);
        if (computingFunction instanceof LazyConstant<? extends T> lc) {
            return (LazyConstant<T>) lc;
        }
        return LazyConstantImpl.ofLazy(computingFunction);
    }

 

return文のところを見ると、LazyConstantImplクラスというのがあることがわかります。これがLazyConstantインタフェースを実装したコンクリートクラスです。パッケージはjdk.internal.langパッケージで、公開されていないクラスだということが分かります。

 

では、遅延初期化を行うgetメソッドを見る前に、クラスの宣言と関連するフィールド、そしてofLazyメソッドを見ておきましょう。この他にもフィールドありますが、関連するところだけ。

 

@AOTSafeClassInitializer
public final class LazyConstantImpl<T> implements LazyConstant<T> {

    @Stable
    private T constant;

    @Stable
    private volatile Supplier<? extends T> computingFunction;

    private LazyConstantImpl(Supplier<? extends T> computingFunction) {
        this.computingFunction = computingFunction;
    }

    public static <T> LazyConstantImpl<T> ofLazy(Supplier<? extends T> computingFunction) {
        return new LazyConstantImpl<>(computingFunction);
    }

 

見慣れないアノテーションが使われていますが、Leyden関連や最適化のヒントになるアノテーションです。

ここで注目しておいていただきたいのが、値を保持するconstantフィールドがvolatileではないということです。そして、ofメソッドの引数のラムダ式はcomputingFunctionフィールドで保持しています。

 

では、保持している値を返すgetメソッドを見ていきましょう。

 

    @ForceInline
    @Override
    public T get() {
        final T t = getAcquire();
        return (t != null) ? t : getSlowPath();
    }

    private T getSlowPath() {
        preventReentry();
        synchronized (this) {
            T t = getAcquire();
            if (t == null) {
                t = computingFunction.get();
                Objects.requireNonNull(t);
                setRelease(t);
                // Allow the underlying supplier to be collected after successful use
                computingFunction = null;
            }
            return t;
        }
    }

 

getメソッドの最初に出てくるgetAcquireメソッドはconstantフィールドを取得するメソッドです。このメソッドについては後でもう一度触れます。

getAcquireメソッドでconstantフィールドをローカル変数のtに代入しています。続いて、tがnullでなければ、tをそのまま返しています。逆に、tがnullならばgetSlowPathメソッドをコールしています。

getSlowPathメソッドの先頭でpreventReentryメソッドをコールしていますが、これはロックを取得している状態で再びロックを取得する(これを再入すると呼びます)ことを防ぐメソッドです。Javaのsynchronizedは再入が可能なロックなのですが、ここではそれを防いでいるということです。

そして、synchronizedでロックを取得し、再びtを取得してnullかどうかをチェックしています。つまりダブルチェックになっているということです。

ダブルチェックをしてtがnullの場合、computingFunctionフィールドに対してgetメソッドをコールしています。これがSupplierのラムダ式の実行を意味しています

つまり、ここで値の初期化を行っています。

初期化した値がnullの場合はrequireNonNullメソッドでチェックしてNullPointerException例外をスローします。これが、前回のエントリーで例外を扱う場合の1の選択肢(ラムダ式でnullを返す)時の挙動になります。

次のsetReleaseメソッドはgetAcquireメソッドの逆です。

そして、computingFunctionにnullを代入しています。つまり、一度Supplierのラムダ式が実行されたら、その後はラムダ式を実行することができないということです。

このようにして、ダブルチェックロックを使って値の初期化を行っています。

しかし、気になるのは、シングルトンでは遅延初期化するフィールドがvolatileだったのにLazyConstantImplクラスではvolatileではないという点です。

この問題はgetAcquireメソッドを見てみれば、理由が分かります。

 

    @SuppressWarnings("unchecked")
    @ForceInline
    private T getAcquire() {
        return (T) UNSAFE.getReferenceAcquire(this, CONSTANT_OFFSET);
    }

 

ここで使われているUNSAFE変数は、一般には使わないようにと言われている危険なjdk.internal.misc.Unsafeクラスです。標準ライブラリだからこそ、使えるということですね。

そして、UnsafeクラスのgetReferenceAcquireメソッドを見てみると...

 

    @IntrinsicCandidate
    public final Object getReferenceAcquire(Object o, long offset) {
        return getReferenceVolatile(o, offset);
    }

 

なんと参照をvolatileで取得するメソッドをコールしていました。

つまり、LazyConstantImplクラスのconstantフィールドはvolatileで定義されてはいないものの、アクセスする時はvolatile相当で行われるということです。

これがconstantフィールドがvolatileで定義されていない理由になります。まぁ、普通にはできない技ですね(そもそもUnsafeクラスは使えないですし)。

 

もう1つ標準ライブラリだからこその技が最適化のヒントとなるアノテーションです。

たとえば、@ForceInlineアノテーションはメソッドのインライン化を行わせるアノテーションです。また、@Stableアノテーションは値が変更されないことを保証して、値を埋め込むなどの最適化を可能にしています。

このような最適化に対するアノテーションを使うことで、実行時最適化をやりやすくしているわけです。

 

まとめ

finalフィールドの遅延初期化を行うLazy Constantの実装を見てきました。

遅延初期化で使われている手法は、シングルトンで使われていたvolatileとダブルチェックロックです。この手法を使う場面というのはなかなかないとは思いますが、知識として知っておくのはよいですね。

そもそも、Lazy Constant自体がそれほど頻繁に使われるAPIではありません。しかし、もしイミュータブルなクラスで遅延初期化をしなければならないような場合はぜひ思い出してやってください。

2025/12/04

Lazy Constant

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

本エントリーはJava Advent Calendar 2025の4日目です。昨日はAsanoさん (@mackey0225) の読書感想文 : 『Javaの10年』でした。

 

11月15日に開催されたJJUG CCC 2025 FallでLazy Constantについてプレゼンしてきました。資料はこちら。

 

Lazy Constantはfinalフィールドを遅延初期化させるためのAPIで、Java 26ではJEP 526で提案されています。

 

JEP 526: Lazy Constants (Second Preview)

https://openjdk.org/jeps/526

 

LazyConstantインタフェース自体は単機能で使い方も簡単なのですが、その導入背景は理解しておいた方がよいと思います。

 

なぜLazy Constant?

Javaでも宣言的なプログラミングスタイルが増えてきたり、並列処理が当たり前に使われるようになってきて、イミュータブル性の重要度が増しています。

また、アーキテクチャー的にもDDDの導入で値クラスを使うことが多くなっています。もちろん、値クラスはイミュータブルです。

 

そこで、イミュータブルなクラスを作ることを考えるわけですが、イミュータブルなクラスの条件の1つにフィールドはすべてfinalにするということがあります。

ここで困るのが、フィールドによっては初期化に時間がかかるものがあることです。たとえば、通信やファイル読み込みなどの外部リソースにアクセスする場合などがこれに相当します。

時間のかかるフィールドの初期化がアプリケーションの起動時にまとまって発生してしまうと、ただでさえいろいろやらなければならない起動時ですが、このようなフィールの初期化のためにさらに起動時間がかかるということになってしまいます。

通常のフィールドであれば、実際にフィールドを使用する時まで初期化を遅らせることができます。しかし、finalフィールドはオブジェクトの生成時にしか初期化することができません。

そこで登場するのがfinalフィールドの遅延初期化をサポートするLazy Constantです。

 

Lazy Constantとは

Lazy Constantは値を1つだけ保持するコンテナのようなものです。

そして、保持する値の初期化は実際に使用する時まで遅延させます。もちろん、スレッドセーフなので並列処理でも使えます。

値の初期化を行うにはSupplierインタフェースを使用します。ようするに、引数なし、戻り値ありのラムダ式ですね。

重要なことが、JVMの最適化を享受しやすい実装になっていることです。LazyConstantオブジェクトから値を取得するときにはgetメソッドを使用する必要があり、しかもgetメソッド内では値が初期化されたかどうかをチェックする必要があります。これらのオーバーヘッドがあるにも関わらず、インライン化などの最適化を行いやすくなっており、最適化後は直接変数にアクセスするのとパフォーマンスが変わらなくなります。

では、このような特徴を持つLazyConstantを使ってみましょう。

 

LazyConstantの使い方

LazyConstantはインタフェースで、パッケージはjava.langです。ですから、import文は必要ありません。

メソッドは4つだけ。しかも、ほぼofメソッドとgetメソッドしか使いません。

  • static LazyConstant<T> of(Supplier<T> computingFunction) : LazyConstantオブジェクトのファクトリーメソッド
  • T get() : 値を取得するためのメソッド

 

ofメソッドでLazyConstantオブジェクトを生成して、getメソッドで値を取得するというだけです。

たとえば、Consumerクラスで遅延初期化させたいHeavyクラスのオブジェクがあるとしましょう。このオブジェクトをLazyConstantインタフェースを使用して遅延初期化させてみます。

LazyConstantインタフェースのフィールドをheavyConstantとしてfinalで定義します。

そして、実際にheavyConstantフィールドが保持する値を使用するのはconsumeメソッドだとします。

public class Consumer {
    private final LazyConstant<Heavy> heavyConstant
            = LazyConstant.of(() -> new Heavy());

    public void consume() {
        // 初回のgetメソッドコール時に
        // ofメソッドの引数で指定したラムダ式を実行し値を初期化
        // 次からは初期化後の値を取得
        var h = heavy.get();
        IO.println(h);

        // heavyを使用した処理...
    }
}

 

ofメソッドの引数のラムダ式では、Heavyオブジェクトの生成を行っています。オブジェクトの生成だけなのであれば、コンストラクター参照を使用してHeavy::newでも大丈夫です。

そして、heavyフィールドが保持している値を取得するためにgetメソッドを使用します。getメソッドの初回コール時にofメソッドの引数で指定されたラムダ式が実行されます。2回目以降は初期化された値が返ります。

 

では、このConsumerクラスを使ってみましょう。

void main() {
    var consumer = new Consumer();

    consumer.consume();
    consumer.consume();
    consumer.consume();
}

 

Heavyクラスのコンストラクターでは初期化しているというメッセージを標準出力に表示しています。

LazyConstantインタフェースはJava 26ではPreview APIなので、コンパイルや実行には--enable-previewが必要です。

>  java --enable-preview .\Main.java
ノート: C:\test\SomeData.javaはJava SE 26のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Heavy Initializing
Heavy@2002fc1d
Heavy@2002fc1d
Heavy@2002fc1d
> 

 

コンストラクターがコールされているのは1度だけで、それ以降はgetメソッドで返る値は同じオブジェクトになっていることがわかります。

 

初期化にデータが必要な場合

遅延させたい値の初期化に何らかのデータが必要になる場合はどうでしょう。

初期化にSupplierインタフェースを使うので、引数で渡すことはできません。しかし、final変数であればラムダ式の外側の変数も参照できるので次のように書くこともできます。

    public Consumer(final String filename) {
        heavyConstant = LazyConstant.of(() ->  new Heavy(filename));
    }

 

Heavyクラスのコンストラクターでは引数の文字列を表示させるようにしてみました。また、mainメソッドでも文字列を渡しています。

void main() {
    var consumer = new Consumer("Main.java");

    consumer.consume();
    consumer.consume();
    consumer.consume();
}

 

では、実行してみましょう。

>  java --enable-preview .\Main.java
ノート: C:\test\SomeData.javaはJava SE 26のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Heavy Initializing: Main.java
Heavy@2002fc1d
Heavy@2002fc1d
Heavy@2002fc1d
> 

 

このようにラムダ式を書いた時点でのfinal変数であるfilename変数をキャプチャーし、ラムダ式の実行時に使用していることが分かります。

 

初期化時の例外

外部リソースにアクセスするから遅延初期化をしたいということはけっこうあると思います。この時に困るのが例外です。

外部リソースへのアクセスはどうしても例外が発生しがちです。こんな時にどうすればよいのか?

考えられるのは以下の3つの選択肢ではないでしょうか。これについては特にJEPで触れられていないので、さくらばだったらこうするという方法です。

 

  • ラムダ式でnullを返す
  • 例外をRuntimeException例外にくるんで、スローする
  • ラムダ式の返り値をEitherもしくはResultなどにする

 

1つ目の方法は初期化のためのラムダ式で例外が発生した時に、nullを返すというものです。ラムダ式でnullを返すと、getメソッドの内部でNullPointerException例外がスローされます。

このNullPointerException例外をキャッチするという方法です。

この方法は簡単でよいのですが、残念ながら原因となる例外が隠されてしまい、スタックトレースなども引き継ぐことができません。このため、NullPointerException例外が発生したとしても、原因を調べるのが難しくなってしまいます。

 

2つ目のRuntimeException例外を使う方法は、初期化のためのラムダ式のSupplierインタフェースが検査例外をスローできないからです。これはjava.util.functionパッケージの他のインタフェースも同じですね。

そこで、何らかの例外が発生したらRuntimeException例外のcauseにセットして、RuntimeException例外をスローするという方法です。

この方法はスローするRuntimeException例外をドキュメント化しておかないと例外処理を忘れがちという問題があります。

少なくとも、マルチスレッドで使用している場合のUncaughtExceptionにならないようにしなくてはいけません。

この点に注意すれば、まぁ使える方法ではないかなと思います。

 

最後のEitherもしくはResultを使う方法は、例外を値として保存しておいて、ラムダ式の返り値として返すという方法です。

たとえば、Eitherであればleftとrightの2つの値を保持できるようになっており、正常に初期化できた時はleftに保持させ、例外が発生した時にはrightに保持させます。そして、このEitherオブジェクトをラムダ式の返り値として返します。

このため、LazyConstantインタフェースのgetメソッドの戻り値はEitherになり、leftとrightのそれぞれの場合の処理を記述します。

しかし、これだとLazyConstantというコンテナにEitherというコンテナを保持させ、実際に使うにはgetしてgetしなければならないというちょっとめんどくさいことになってしまいます。

こう考えると、finalフィールドのためにEitherを使うのはちょっとやりすぎではないかなと、さくらばは思うわけです。

 

ということで、さくらば的には2つ目のRuntimeException例外を使うかなぁ...

 

コレクションの遅延初期化

LazyConstantインタフェースではコレクションを初期化するのはちょっと難しいのですが、ListインタフェースとMapインタフェースに遅延初期化するためのメソッドが追加されています。

  • static List<E> List.ofLazy(int size, IntFunction<? extends E> computingFunction)
  • static Map<K, V> Map.ofLazy(Set<? extends K> keys, Function<? super K, ? extends V> computingFunction)

 

たとえば、0から9までを保持するリストを遅延初期化させるのであれば、次のように記述します。

private final List<Integer> nums = List.ofLazy(10, i -> i);

 

ofLazyメソッドを使用して生成したコレクションは、実際にそのコレクションにアクセスした時にofLazyメソッドの引数で指定されたラムダ式を実行して初期化を行います。

 

その他のメソッド

使うことはほぼないとは思いますが、LazyConstantインタフェースの残りの2つのメソッドについても簡単に触れておきましょう。

 

boolean isInitialized()

メソッド名で分かると思いますが、保持している値が初期化が行われたかどうか、つまりgetメソッドが1度でもコールされたかどうかを判定するためのメソッドです。初期化されて入ればtrue、初期化されていなければfalseが返ります。

 

T orElse(T other)

保持している値が初期化されていればその値を返し、初期化されていなければ引数のotherを返します。

注意しなければいけないのは、orElseメソッドの内部ではgetメソッドはコールされません。つまり、値が初期化されていなくても、初期化することはありません。

くり返しますが、初期化されていない場合はotherを返します。

 

まとめ

LazyConstantインタフェースはfinalフィールドを遅延初期化させたい場合に使用するインタフェースです。

それほど使うインタフェースではないとは思いますが、ロガーをfinalフィールドで定義したい場合などに使えます。

遅延初期化の機能だけに注目すれば、finalでないフィールドに対しても使うことが可能です。

とはいうものの、遅延初期化はそれなりにオーバーヘッドがあるので、遅延初期化を必要としないフィールドに対して使うのはやりすぎです。

遅延処理を必要とする場合に使うようにしましょう。

 

使い方だけで、意外に長くなってしまったので、LazyConstantインタフェースでの遅延処理の実装について次回説明することにします。

2025/10/11

JEPで語るJava 25 その2

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

前回は、Java 25のJEPのうち初心者向け機能と小さいオブジェクトを扱う機能を紹介しました。今回はその続きです。

まずはProject Leyden関連のJEPです。

 

Project Leyden

Project Leydenは、Javaの起動時間を短縮することを目標にしたプロジェクトです。

さくらばは、てっきりGraalのネイティブイメージを取り込むだけなのかと思っていたのですが、そうではなく、様々な手段を用いて起動時間とピークパフォーマンスに達するまでの時間を短縮するための機能を提供するようです。

Java 24ではJEP 483が導入されましたが、Java 25はこの続きになるJEPが2つ導入されました。

 

JEP 514: Ahead-of-Time Command-Line Ergonomics

JEP 483はクラスロード、クラスの解析、クラス間のリンク、static初期化を行った後のイメージをキャッシュとして保存して、起動時にそれを読み込むことによって起動時間を短縮させます。

このキャッシュ(AOTキャッシュと呼ばれます)を作るには、トレーニング実行を行って、その後キャッシュ作成するという2段階が必要でした。

しかし、起動時オプションを変えて、2回実行するというのはちょっとめんどう...

ということで、JEP 514は1度の実行でトレーニング実行とAOTキャッシュ作成を行ってしまいましょうというものです。

AOTキャッシュを作成するには以下のように実行します。

 

  • AOTキャッシュ作成
    $ java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...

AOTキャッシュを使って実行するのは、JEP 483と同じです。

  • 本番実行
    $ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

 

もちろん、JEP 483の2段階でAOTキャッシュを作成することも可能です。

 

JEP 515: Ahead-of-Time Method Profiling

HotSpot VMはアプリケーションを実行しながら、その片手間にプロファイリングもやっています。プロファイルイングの結果から、実行中にメソッドをネイティブコードにコンパイルするなどの最適化を行っています。

逆にいうと、ピークパフォーマンスに達するまでに時間がかかるということです。

Leydenで起動時間を短縮するのが必要なユースケースでは、なおさらピークパフォーマンスに達する時間が問題になりそうです。

そこで、JEP 515では、AOTキャッシュを作成するトレーニング実行中に作成したプロファイリングの解析結果をAOTキャッシュに含めるようになりました。

AOTキャッシュを使って実行すると、プロファイリングを行わずにすぐにネイティブコンパイルなどの最適化を行うことができるので、ピークパフォーマンスに到達する時間を大幅に短縮することができます。

 

Project Leydenでは、この後、JEP 516: Ahead-of-Time Object Caching with Any GCやまだドラフトですがJEP draft: Ahead-of-Time Code Compilationなどが控えています。

 

これらのProject Leydenの機能に関して、9月のJJUGナイトセミナーでOracleのじゅくちょー阪田さんが解説してくれたのですが、その資料がまだ公開されていない...

もし、LeydenのAOTキャッシュに興味があるのであれば、資料が公開されるのを待ちましょう!

 

安全性、セキュリティ

JEP 510の鍵導出関数はとりあえずおいておいて、サポートできなくなったポーティングは削除しましょうというのがJEP 503です。

 

JEP 503: Remove the 32-bit x86 Port

32bit版のWindowsのサポートが今月終了するということで、Java 24で32bit版のWindowsのポーティングが削除されました。また、x86のポーティングも削除予定になっていました。

そして、Java 25でx86のポーティングが削除されました。

サポートされないOSに対しては安全性の問題もあるので、削除はしかたないですね。

 

モニタリング

OpenJDKでのモニタリングとプロファイリングといえば、JDK Flight Recorder (JFR)ですね。

Java 25では、このJFR関連のJEPが3つあります。1つはExperimentalなので後述することにして、残りの2つについて簡単に紹介します。

とはいうものの、1つは内部実装の話なので、機能という感じではないですけど。

 

JEP 518: JFR Cooperative Sampling

非同期にスレッドのスタックトレースをサンプリングする部分の実装を見直して、安定性を高めたというJEP。

使い方の変更はないので、Java 25のJFRであれば安定してスタックトレースを取得できるようになるはずです。

 

JEP 520: JFR Method Timing & Tracing

メソッドがコールされたタイミングや回数などをサンプリングではなく、実測で調べるためのJFRのイベントが追加されました。

追加されたイベントはjdk.MethodTimeingとjdk.MethodTraceの2種類です。

これらのイベントを使用することで、メソッドがコールされたタイミングとメソッドの処理時間、そしてスタックトレースを取得することができます。

 

その他

Standard JEPの残った2つは、Project Loom関連のScoped Valueと、GCのShenandoah GCに関連したJEPです。

 

JEP 506: Scoped Values

ThreadLocalクラスはミュータブルでいろいろと問題のあるクラスですが、それをある程度置き換えることができるのがScopedValueクラスです。

すべてではないのはScopedValueクラスがイミュータブルなクラスなので、ThreadLocalクラスでsetメソッドを使っているようなケースは置き換えられないからです。

 

使い方は簡単で、ScopedValueインスタンスをnewInstance()メソッドで作成しておいて、そこにstaticメソッドのwhere()メソッドで保持する値をバインドさせます。

where()メソッドの返り値の型はScopedValue.Carrierクラスです。このScopedValue.Carrierクラスのrun()メソッド、もしくはcall()メソッドで実行するタスク内だけでScopedValueオブジェクトにバインドしたデータを使用できます。

つまり、run()メソッドとcall()メソッドで指定する関数だけがスコープになるわけです。

 

たとえば、時間を共有したい場合を考えてみます。

// スコープで共有するScopedValueオブジェクト
final ScopedValue<LocalDateTime> TIME = ScopedValue.newInstance();

void task1() {
  // get()メソッドで共有したデータを取得
  System.out.println("Task1: " + TIME.get());
}

void task2() {
  // get()メソッドで共有したデータを取得
  System.out.println("Task2: " + TIME.get());
}

void main() {
  Runnable r = () -> {
    // whereメソッドでTIMEに現在時刻をバインド
    // runメソッドで指定するラムダ式の中だけでバインドしたデータを共有
    ScopedValue.where(TIME, LocalDateTime.now())
               .run(() -> {
                  task1();
                  task2();
                });

  };

  try (var executor = Executors.newCachedThreadPool()) {
    executor.submit(r);
  }
}

 

とはいうものの、ThreadLocalクラスやScopedValueクラスを使ったデータの共有はできるだけ避ける方がよいです。共有したデータのせいでスケールしないことも多いですし、バグも発生しやすいです。

ScopedValueクラスを安易に使うよりは、もう一度設計を見直すことの方が賢明だと思います。

たとえば、上のコードでもtask1()メソッドとtask2()メソッドの引数として時間を渡した方が分かりやすいですよね。共有せずに済むのであれば、共有しない設計にすべきです。

 

JEP 521: Generational Shenandoah

Red Hatが進めていたShenandoah GCは、以前のZGCのように世代別GCではなかったのですが、ZGCの後を追うように世代別GCになりました。

Shenandoahで世代別モードにするには、まず実行時オプションの-XX:+UseShenandoahGCでGCにShenandoahを指定します。そして、同じく実行時オプションで-XX:ShenandoahGCMode=generationalを指定します。

 

Preview

ここからはお試し機能。まずは、Preview JEPからです。

Preview JEPは4種類。JEP 507はJava 25でStandardになると予想していたんですけど、Previewでしたね。残念。

 

JEP 470: PEM Encodings of Cryptographic Objects (Preview)

1つ目のPreview JEPは鍵導出関数に関するAPIです。

 

JEP 502: Stable Values (Preview)

StableValueクラスはfinalなフィールドに対して遅延初期化を行うためのクラスです。

それにしても、java.langパッケージにScopedValueクラスとStableValueクラスという似たようなクラスができると混乱するよなぁ... と思っていたら、Java 26がターゲットのJEP 526ではLazyConstantクラスに名前が変わりました!

LazyConstantクラスに関してはJJUG CCC 2025 Fallでプレゼンすることになったので、その時に詳しく解説します。

 

JEP 505: Structured Concurrency (Fifth Preview)

Structured ConcurrencyはなかなかStandard JEPになりませんが、複数の非同期実行のタスクの結果をまとめることなどに使用するAPIです。

Java 25で5回目のPreviewですが、APIに変更があったためJava 26でもう一度Previewということになっています(JEP 525)。

Java 24まではStructuredTaskScopeクラスのインスタンシエーションにはnewを使用していたのですが、Java 25でファクトリーメソッドのopen()メソッドが導入されています。

また、全部成功したらとか、1つでも失敗したらなどの条件を表すために、Java 24まではStructuredTaskScopeクラスのサブクラスで表していましたが、JEP 505ではStructuredTaskScope.Joinerインタフェースに委譲するようになりました。この方が分かりやすいですし、今後条件の拡張しやすくていいですね。

 

JEP 507:Primitive Types in Patterns, instanceof, and switch (Third Preview)

パターンマッチングにプリミティブ型を使用できるようにするのがJEP 507です。

JEP 507は変更なしだったのですが、まだドラフトなのですが次のJEPで変更が入ったので、Standardになるのは当分先になってしまいました。残念。

 

Experimental

Experimental JEPは1つだけで、JFRの機能追加に関してです。

 

JEP 509: JFR CPU-Time Profiling (Experimental)

CPUの計測を行うJFRのイベントが追加されました。ただし、Linuxだけです。

イベントはjdk.CPUTimeSampleです。

今のところ、新しいJEPは提案されていないようなので、Java 26でStandard JEPになるかもしれません。

 

Incubator

最後のIncubator JEPは、Value Classが導入するまでずっとIncubatorのままのあれです。

 

JEP 508: Vector API (Tenth Incubator)

ベクトル計算を行うためのVector APIですが、JEP 508ではいくつかの変更がありました。ただ、Value Classが導入されるまでIncubatorのままなので、今それを調べてもなぁ... と調べるモチベーションが上がりません😰

 

まとめ

Java 25はLTSということもあり、Standard JEPが多いリリースでした。

特にJEP 512のメインクラスの省略は意外に便利です。また、JEP 506のScoped Valueも正式化したので、今後ThreadLocalの置き換えが進むかもしれません。

 

ところで、Java 25とは関係ないのですが、ずっとドラフトのままだったValue Classがサブミットされて、通常のJEPにやっとなりました。JEP 401: Value Classes and Objectsです。

まだ、ターゲットバージョンが決まっていないのですが、早ければJava 26からPreviewが始まるかもしれません。

いつ導入されるのか全然わからなかったValue Classですが、やっと一歩踏み出した感じですね。

2025/10/04

JEPで語るJava 25 その1

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

さて、Java 25のJEP (JDK Enhancement Proposal)です。

Java 25のJEPは18個。そのうち、Standard JEPが12で、Preview JEPが4、Experimental JEPとIncubator JEPがそれぞれ1つという内訳です。

ちなみに、gihyo.jpでJEPの概要について記事を書きましたので、ざっと分かればよいという方はこちらをご参照ください。

 

Java 25リリース 初心者向け機能や起動時間短縮など

https://gihyo.jp/article/2025/09/java25

 

Java 25のJEPの一覧を次に示します。

  • 470: PEM Encodings of Cryptographic Objects (Preview)
  • 502: Stable Values (Preview)
  • 503: Remove the 32-bit x86 Port
  • 505: Structured Concurrency (Fifth Preview)
  • 506: Scoped Values
  • 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)
  • 508: Vector API (Tenth Incubator)
  • 509: JFR CPU-Time Profiling (Experimental)
  • 510: Key Derivation Function API
  • 511: Module Import Declarations
  • 512: Compact Source Files and Instance Main Methods
  • 513: Flexible Constructor Bodies
  • 514: Ahead-of-Time Command-Line Ergonomics
  • 515: Ahead-of-Time Method Profiling
  • 518: JFR Cooperative Sampling
  • 519: Compact Object Headers
  • 520: JFR Method Timing & Tracing
  • 521: Generational Shenandoah

gihyo.jpの記事でも、関連したJEPをまとめて扱いましたが、ここでもそうしていきます。

  • 初心者向け: 511, 512
  • 小さいオブジェクト: 513, 519
  • Project Leyden: 514, 515
  • 安全性、セキュリティ: 503, 510
  • モニタリング: 518, 520
  • その他: 506, 521
  • Preview: 470, 502, 505, 507
  • Experimental: 509
  • Incubator: 508

なお、セキュリティに関するJEP 470とJEP 510は、いつものごとく説明を省略します。

 

初心者向け

今までの初心者向け機能というと、Javaのソースファイルをコンパイルしなくても実行することができるJEP 330とJEP 458がありました。

これらは、コンパイルして実行しなくてはいけないという手間を減らすための機能です。

しかし、Javaが初心者向けではないといわれるもう1つの要因が残っていました。たとえば、"Hello, World!"を出力するだけのプログラムでもメインクラスを書かなくてはいけないなど、どうしても記述量が多くなってしまうという点です。

それに対し、Java 25では2つのJEPでこれに対応しています。

 

JEP 511: Module Import Declarations

IDEを使ってプログラムを書いていると、import文はIDEが勝手に整理してくれるのでほとんど気にしないですけど、初心者にとってはハードルが高いのも確か。

あっという間にimport文が何十行にもなっていたりするんですよね。しかも、なぜかimport文でアスタリスクを使うのは嫌われているし。あれは何で嫌われているんでしょうね?

それに対し、JEP 511はimport文をモジュール単位で指定できるようにしましょうというJEPです。

書き方は簡単です。Mモジュールで定義されたクラスやインタフェースなどを使用する場合、次のように記述します。

import module M;

 

moduleキーワードが入るだけですね。

たとえば、GUIでAWTやSwingを使いたいのであれば、次のようになります。

import module java.desktop;

 

しかし、AWTを使うと、Listが問題になります。ようするに、java.awt.Listなのか、java.util.Listなのかという問題です。

import module java.desktop;
import module java.base;

  ...

  List list = ... // これは java.awt.List か java.util.List か?

 

こういう時は、モジュールインポートより通常のインポート文の方が優先されるので、次のように書きます。

import module java.desktop;
import java.util.List;

  ...

  List list = ... // これは java.util.List

 

あくまでも説明のためなので、RAW型を使うなとか言わないでくださいね。

また、アスタリスクで指定でも大丈夫です。

 

import module java.desktop;
import java.util.*;

  ...

  List list = ... // これは java.util.List

 

単一のインポート > アスタリスクでインポート > モジュールインポート の順に優先度が高くなります。

JEP 511ではあいまいなインポートを解決するためにインポート文を記述するのをシャドーイングという言葉で説明しているます。そういわれれば分かるものの、さくらばには、そのニュアンスはなかなか理解できないのでした。

 

JEP 512 Compact Source Files and Instance Main Methods

もう1つの初心者向け機能がJEP 512です。

JEP 512ではプログラム実行の起点であるmainメソッドを簡略化するための仕様を取り決めています。

簡略化できるのは次の4項目です。

  • メインクラスの省略
  • mainメソッドの簡略化
  • java.baseモジュールのインポートの省略
  • 標準入出力を行うクラスの導入

 

では、おなじみのHello, World!で考えてみましょう。ここではHello.javaファイルに記述しているものとします。

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

 

メインクラスの省略

プログラムの実行という意味では起点となるmainメソッドだけあればよいのですが、Javaでメソッドだけ単体で定義するわけにはいかなかったので、メインクラスが必要でした。

これに対して、Java 25ではメインクラスが省略可能になりました。

public static void main(String[] args) {
    System.out.println("Hello World");
}

 

これだけでかなりスッキリしますね。

 

mainメソッドの簡略化

メインクラスが省略できるようになったのに、わざわざアクセス修飾子のpublicやstaticなどは書く意味がありません。しかも、初心者にpublicやstaticを教えるのもなかなか大変なので、できれば触れたくない部分でもあります。

ということで、Java 25ではmainメソッドも大幅に簡略して書くことができるようになりました。

メインクラスも省略すると、"Hello, World!"を出力するには以下のようになりました!

void main() {
    System.out.println("Hello World");
}

 

アクセス修飾子もstaticも引数も省略可能です。もちろん、書いてもだいじょうぶです。

 

さくらばの予想では、省略してもコンパイル時に補完されてpublic static void main(String... args)としてクラスファイルに記述されると思っていました。

ところが、クラスファイルをjavapで見てみるとvoid main()のままでした。

ということは、javaの起動時にmainという名前のメソッドを検索して実行するように動作が変わったようです。

 

では、mainメソッドがオーバーロードされていたらどうなるでしょう。

void main(String arg) {
    System.out.println("Hello World 3");
}

void main(String... args) {
    System.out.println("Hello World 2");
}

void main() {
    System.out.println("Hello World 1");
}

 

さて、実行すると1、2、3のどちらが出力されるでしょう。

> java Hello.java
Hello World 2
    
>

 

やはり、元々のpublic static void main(String... args)に近いシグネチャーのオーバーロードが選ばれるようです。

 

ところで、void main()はstaticがついていないので、クラスファイルにはインスタンスメソッドとして定義されています。

この場合、他のメソッドはコールできるのでしょうか?

void hello() {
    System.out.println("Hello, World!");
}

static void staticHello() {
    System.out.println("Hello, Static World!");
}

void main() {
    hello();
    staticHello();
}

 

インスタンスメソッドとstaticメソッドを定義してコールしてみました。実行してみると...

> java Hello.java
Hello, World!
Hello, Static World!
    
>

 

インスタンスメソッドもstaticメソッドもコールすることができました。

ただし、mainメソッドをstaticで定義してしまうと、インスタンスメソッドをコールする部分でコンパイルエラーになります。

void hello() {
    System.out.println("Hello, World!");
}

static void staticHello() {
    System.out.println("Hello, Static World!");
}

static void main() {
    hello();
    staticHello();
}

 

> javac Hello.java
Hello.java:10: エラー: staticでないメソッド hello()をstaticコンテキストから参照することはできません
    hello();
    ^
エラー1個
    
>

 

java.baseモジュールのインポートの省略

次はインポート文の簡略化です。

リストの要素の一覧を出力するプログラムを考えてみましょう。

Listインタフェースを使うのですから、import java.util.List;が必要になりますが、JEP 511のおかげでimport module java.base;で済みます。

ところが、このインポート文は省略することができます。java.langパッケージのインポートは省略できるのと同じように、java.baseモジュールのインポートを省略できるのです。

したがって、次のようにListインタフェースを使用していても、インポート文は書かずに済みます。

void main() {
    List.of("Alpha", "Bravo", "Charlie")
        .forEach(System.out::println);
}

 

ただし、これはメインクラスを省略した場合だけです。

class Hello {
    void main() {
        List.of("Alpha", "Bravo", "Charlie")
            .forEach(System.out::println);
    }
}

 

> javac Hello.java
Hello.java:3: エラー: シンボルを見つけられません
        List.of("Alpha", "Bravo", "Charlie")
        ^
  シンボル:   変数 List
  場所: クラス Hello
エラー1個
    
>

 

メインクラスを書くのであれば、インポート文も記述しましょう。

import module java.base;

class Hello {
    void main() {
        List.of("Alpha", "Bravo", "Charlie")
            .forEach(System.out::println);
    }
}

 

> java Hello.java
Alpha
Bravo
Charlie
    
>

 

標準入出力を行うクラスの導入

最後が標準入出力を行うクラスです。

意外にSystem.outとSystem.inは説明するのが難しいんですよね。そこで、標準出力と入力の両方を扱うためのjava.lang.IOクラスが導入されました。

IOクラスでは、標準入出力のための最低限のメソッドだけが定義されています。もちろん標準エラー出力も扱いません。

定義してあるメソッドは以下の5種類です。

  • static void print(Object obj)
  • static void println()
  • static void println(Object obj)
  • static String readln()
  • static String readln(String prompt)

 

一番使うのはprintln(Object obj)でしょうね。標準入力もあるので、次のようなプログラムも簡単に記述できます。

void main() {
    for(;;) {
        var to = IO.readln("To: ");
        IO.println("Hello, " + to + "!");
    }
}

 

> java Hello.java
To: Wolrd
Hello, Wolrd!
To: Java
Hello, Java!
To:

 

ここまで省略できたり簡略化されたりすると、かなり書くのも楽になりますね。

 

小さいオブジェクト

 

Javaは歴史的経緯もあって少数のミュータブルで巨大なオブジェクトを扱うことが多かったのですが、Java SE 8から関数型の機能が導入されてから多数のイミュータブルな小さいオブジェクトを扱うように変わってきています。

イミュータブルな小さいオブジェクトを効率的に扱うようにするのが、Project ValhallaのValue Classです。

しかし、Value Classの導入には言語仕様やVM仕様の大幅な変更が伴うため、おいそれと導入するわけにはいきません。そこで、Value Classがまとまる前に、できるものから導入していこうというのが現在のステータスです。Java 25ではJEP 513: Flexible Constructor Bodiesが、それに相当します。

もう一方のJEP 519: Compact Object HeadersはValue Classとは直接関連はないのですが、多量の小さいオブジェクトを扱うためにオブジェクトヘッダーをコンパクト化しましょうというJEPになります。

 

JEP 513: Flexible Constructor Bodies

Java 24の時にも書きましたが、コンストラクター内でスーパークラスや自分自身のコンストラクターをコールするのは、コンストラクターの先頭と決まっていました。

これに対し、自分自身のフィールドの初期化を行ってから、他のコンストラクターをコールできるようになりました。

Value Classはイミュータブルなクラスですが、今までのオブジェクトの初期化順だと、初期化前のフィールドにスーパークラスからアクセスできてしまうという問題があります。

問題になるようなことはほとんどないはずですが、イミュータブルなオブジェクトにとっては値が不定な時にアクセスできてしまうというのは困ってしまいます。

そこれで、JEP 513でValue Classが導入される前に、この問題ををつぶしておこうというわけです。

 

JEP 519: Compact Object Headers

オブジェクトヘッダーをコンパクト化するJEP 519は、Java 24のJEP 450と特に変更はないようです。なので、Java 24のJEPで語るをご覧いただければと思います。

結局、Value Classのビットはそのままになったようです。

また、JEP 519にはJava 24での実行時オプションがのったままになっているのですが、これはJava 25でもそのままなのかどうかイマイチ分かりません。

デフォルトでコンパクトヘッダーになっているかどうかは、小さいオブジェクトをいっぱい使って、JFRとかでヒープの使用量を見れば分かるかな? 時間があれば、やってみます。

 

長くなってしまったので、残りは次回!