- JEP 401: Value Classes and Objectsが実際にJDK previewに入る段階に到達
- 中核目標は、Javaオブジェクトを「クラスのようにコーディングし、intのように動作」させ、オブジェクトヘッダー・ヒープ割り当て・GC・ポインタ間接参照のコストを減らすこと
- JDK 28のvalue classはまだnull可能な参照型であり、non-null型・特殊化ジェネリクス・128ビットエンコーディングは含まれず、
--enable-previewが必要
- JVMはvalue objectをスカラ化したり、フィールド・配列にヒープ平坦化したりできるが、erased genericや
Objectのような上位型ではヒープオブジェクトとしてmaterializeされることがある
- Java開発者はidentityとvalueの違いをコード設計に反映させる必要があり、
==、synchronized、primitive wrapper、配列性能、将来のジェネリクス特殊化にまで影響が及ぶ
JDK 28に入るValhallaの範囲
- 6月15日、OracleエンジニアのLois FoltanがJEP 401: Value Classes and ObjectsのOpenJDKメインリポジトリ統合とJDK 28ターゲットを確認
- 関連するpull requestは1,816ファイルにわたり、19万7,000行超を追加
- 変更規模が大きいため、統合中に他のコミッターへ大きなコミットを一時保留してほしいとの要請があった
- JEP 401はデフォルトで無効のpreview機能
- 構文を使うには
--enable-previewが必要
- Brian Goetzはこれを「Valhallaの最初の一部」だと位置付けた
- JDK 28は2027年3月リリース予定で、mainline統合は2026年7月ごろに計画されている
Valhallaが狙うJavaオブジェクトモデルのコスト
- Valhallaの標語は「codes like a class, works like an int」
- メソッド、コンストラクタ検証、意味のあるフィールド名を持つ通常のクラスを使いながらも、JVMがprimitiveのように効率よく扱えるようにするのが目標
- Javaでは8つのprimitiveを除けば、ほぼすべてが参照型
Point p = new Point(1, 2)で、pはpoint自体ではなくヒープオブジェクトを指すポインタ
- フィールドを読むたびにJVMはポインタをたどらなければならない
- オブジェクト数が増えるとコストは急激に大きくなる
- 各オブジェクトには型、同期状態などのためのオブジェクトヘッダーがある
- オブジェクトはヒープに割り当てられ、その後GC対象になる
Pointの100万個配列は、実際には100万個のポインタとヒープの各所に散らばる100万個のオブジェクトで構成される
- Brian Goetzの「State of Valhalla」はこうしたメモリ配置をfluffyと呼ぶ
- Valhallaが目指すのは、データが横並びに置かれるdenseな配置
ハードウェア格差とescape analysisの限界
- 高密度なメモリ配置が重要な理由は、CPUとメモリ速度の格差にある
- 1995年にはメモリアクセスのコストはCPU演算と近かった
- 現在のCPUはメインメモリより一桁以上速く、その差をキャッシュが埋めている
- CPUは通常、64バイトのcache line単位でメモリを読む
- データが高密度で順序よく並んでいれば、一度に有用な値を多く取り込める
- ポインタをたどって散在するオブジェクトにアクセスするとcache missが起こり得て、hitよりはるかに遅くなることがある
- JVMのescape analysisは一部のオブジェクト割り当てを除去できる
- オブジェクトがローカルなコード片の外へ「escape」しないと判断されれば、ヒープに割り当てず、フィールドを変数やレジスタに展開できる
- ただしescape analysisは予測しにくく脆弱
- オブジェクトが別クラスのフィールドに入ったり、配列に保存されたり、複雑なメソッドに渡されたり、JITが解析できない境界を越えたりすると最適化が止まることがある
- 小さなリファクタリング、JDK更新、コード構造の変更だけでも、オブジェクトが再びヒープに載る可能性がある
- 性能のためにオブジェクトを捨てて
r、g、bのようなraw byteで直接エンコードすれば速度は得られるが、安全性・可読性・検証・メソッドを失う
2014年の開始とQ WorldからL Worldへの転換
- Project Valhallaは公式には2014年に始まった
- James Goslingは当時これを「six PhDs tied into a single knot」と表現した
- Javaの創始者たちはJava 1.0の時代からvalue typeを望んでいたが、1995年当時は問題が難しすぎて断念した
- 初期目標は、プログラミングモデルと現代ハードウェアの性能特性の整合を取り戻すことだった
- ユーザーがprimitiveのようにflatでdenseな型を直接宣言しつつ、通常のクラスのように見え、動作するようにする方向
- 初期prototypeはQ World路線だった
- 新しいvalue typeをオブジェクトとは根本的に異なる存在とみなし、別個のtype descriptor、bytecode、top typeを持たせる方式
- JVM型システム全体が2種類のバリエーションを持つ必要があり、複雑さが増した
- 2019年ごろに登場したL Worldが転換点になった
- value typeが通常のreferenceと同じ「L carrier」を共有する
- チームはこの統合が難しいと予想していたが、大きな妥協なしに機能し、以前のprototypeの多くの問題を解決した
- L Worldでは重要な分離が生まれた
- JVMモデルと言語モデルが100%重なる必要はない
- JVMにはL Worldモデルを置き、プログラマーにはより使いやすい言語モデルを提供できる
- その後の作業は、value classと特殊化ジェネリクスという2段階に分かれた
名称とモデルの変遷
- Valhalla の用語は何度も変わっており、単なる名称変更ではなくモデル変更を反映している
- 初期の用語は value types だった
- 当時は、これらの型が正確にどのような存在なのかはまだ明確ではなかった
- 2019〜2020年ごろに inline classes モデルが定着した
- 既存のクラスは identity classes、新しいクラスは identity を持たない inline classes として区別された
- inline class は基本的に final で、フィールドは final、同期化できないという制約が設けられた
- 2021年の「State of Valhalla」では primitive classes と 2 つの projection モデルが扱われた
- 1 つの型が、フラットで null 不可の value variant と、null を許容する reference variant を持つ構想だった
Point.val / Point.ref、その後 Point! / Point? のような構文も試された
- このモデルは強力だったが、認知負荷 が大きかった
- プログラマが同じ型の 2 つの形態と変換のタイミングを日常的に理解する必要があった
- 最終的にはユーザーモデルを単純化するため、dualism は縮小された
- 現在の JEP 401 では value class と value object が使われている
value modifier で value class を宣言する
- インスタンスは identity を持たない value object である
- value class は依然として reference type である
- non-nullability は、別の optional JEP である Null-Restricted Value Class Types に分離された
- 以前の「primitive classes」モデルを説明する古い記事は、現在の OpenJDK の基準とは異なる可能性がある
- JEP 401 には preview の JEP 402: Enhanced Primitive Boxing も含まれている
- primitive と wrapper の間の変換をより滑らかにする方向である
- 完成形として JEP 401 と一緒にすべて入ると想定してはいけない
JDK 28 の value class モデル
- value class は
value modifier で宣言する
value class USDCurrency implements Comparable<USDCurrency> {
private int cents; // implicitly final
public USDCurrency(int dollars, int cents) {
this.cents = dollars * 100 + cents;
}
public USDCurrency plus(USDCurrency that) {
return new USDCurrency(0, this.cents + that.cents);
}
// dollars(), cents(), compareTo(), toString()...
}
- value record も可能である
- 主なルールは次のとおりである
- すべての instance field は 暗黙的に final である
- method は
synchronized にできない
- クラスは基本的に final である
- value class と abstract value class で構成される階層は可能である
- identity を持つクラスを継承することはできない
- interface の実装は可能である
- 中核的な特性は identity を持たないこと である
- 通常のオブジェクトは、同じ内容を持っていても
new Point(1, 2) を 2 回作れば互いに別オブジェクトである
- value object には、
int の値 4 に「別々の 2 つの 4」がないのと同様に identity がない
==、synchronized、null の変化
- value object における
== は identity 比較ではなく、substitutability の検査になる
- 同じクラスで、同じフィールド値を持つかどうかを再帰的に比較する
- primitive field はビット単位で比較し、object field は再び
== で比較する
new USDCurrency(3,95) == new USDCurrency(3,95) は true になる
- ただし
== は内部状態を見るため、「同じデータを表しているか」については通常 equals のほうが適している
- value object には同期化するための identity がない
- 同期化を試みると IdentityException が発生する
- identity を強制的に確認する必要がある場合は
Objects.requireIdentity と Objects.hasIdentity を使える
- JDK 28 の value class は依然として null 許容 である
USDCurrency d = null; は合法である
- null を禁止する型は別の将来 JEP であり、JDK 28 には含まれない
- non-nullability は単なる構文上の問題ではなく、より大きな value class のフラット化を開く 性能レバー になる
スカラ化とヒープ平坦化
- JEP 401 は、JVM が value object を最適化できる自由度を与える
- スカラ化(scalarization) は、value object reference をフィールド集合に分解する JIT 手法である
Color ポインタを渡す代わりに、r、g、b の byte と null かどうかの flag を渡せる
- 割り当てと GC のコストが消える可能性がある
- escape analysis に似ているが、より予測可能で、inline 化されていない method call の境界を越えて適用できる
- スカラ化には制限がある
- 変数型が value class の supertype である
Object や erased generic parameter の場合、通常は機能しない
- この場合、オブジェクトはヒープ上に materialize されなければならない
- ヒープ平坦化(heap flattening) は、value object のフィールド値を compact bit vector としてエンコードし、フィールドや配列セルに直接書き込む方式である
- 別のヒープ位置を指すポインタは不要である
- データ密度と locality が得られる
- 平坦化されたデータは、concurrent access で tearing を避けるため atomic に読み書きできなければならない
- 一般的なプラットフォームで「十分に小さい」サイズは、null flag を含めて 64 ビット程度になりうる
- 小さな value class はうまく平坦化できるが、
int フィールド 2 つや double 1 つだけでも atomic write のサイズに合わず、通常のヒープオブジェクトになる可能性がある
- 将来的には、128 ビットエンコーディングと null-restricted type によって、より大きな value class の平坦化が可能になるかもしれない
Boxing、wrapper、配列での効果
- preview を有効にすると、
Integer、Long、Double のような primitive wrapper class 自体が value class になる
- box が identity を失うため、JVM はそれをスカラ化および平坦化できる
Integer[] は int[] の効率に近づき、boxing overhead が大幅に減る方向になる
- JEP 402: Enhanced Primitive Boxing は primitive と box の間の変換をさらに拡張する
List<int> のような表現への道を開くが、まだ別個に成熟中の取り組みである
- 効果は配列で最もよく表れる
- 従来の
Color[] は、100 万個のポインタとヒープ上に散らばった 100 万個のオブジェクトになる可能性がある
- value class の
Color[] は、連続した色値を直接格納する contiguous block になりうる
- CPU は cache line 単位で複数の値を順次読み取れる
Point[] の例で見る前後の違い
- Valhalla 以前の通常の class の例は次のとおりである
final class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
- この配列は100万個のポインタを保持する
- 各ポインタはヒープ上のどこかにある別個の
Pointオブジェクトを指す
- 各オブジェクトには2つの
intに加えてオブジェクトヘッダもある
- 走査時にはポインタを読み、そのアドレスへジャンプし、フィールドを読まなければならない
- Valhalla以後のvalue classの例は次のとおり
value class Point {
final int x;
final int y;
Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];
- コード上の違いは
valueという1語だけだが、メモリレイアウトは変わる
- JVMは各pointの値を配列内に高密度で格納できる
- 要素ごとのヘッダはなく、ポインタもない
x、yの2つのintを基準に8バイトと、必要に応じてnullフラグで連続配置できる
- 保守性も維持される
Pointは依然として名前、コンストラクタ、検証、methodを持つclassである
int[] xs、int[] ysに分割してインデックスを合わせる方式を避けられる
特化ジェネリクスがまだ残っている理由
- Javaのgenericsはtype erasureで実装されている
List<String>とList<Integer>はruntimeでは同じListである
- type parameter
TはObjectにerasureされる
- erasureは、既存のJavaコードベースを壊さずにgenericsを導入するための意図的な選択だった
- non-generic classをgenericに変えても、既存のsource fileやcompiled classを壊さないようにする
- Valhallaとerasureは性能面で衝突する
List<Point>にvalue objectを入れると、TがObjectにerasureされるため、オブジェクトをヒープ上にmaterializeしなければならない
Point[]で得られるフラット化の利点がArrayList<Point>では失われる可能性がある
- 復旧計画は2段階である
- Universal Generics: 言語レベルでtype variableがvalue typeまで扱えるようにする
- それでもerasureは使う
Tフィールドがデフォルトでnullから始まる問題のため、“null pollution” compiler warningが発生する可能性がある
- 警告を解消すれば、そのAPIはspecialization-readyに近づく
- Specialized Generics: JVMレベルでconcrete type argumentごとの特化されたclass layoutを生成する
- プロジェクト用語ではspeciesとtype restrictionが関連する
- この段階になって初めて
ArrayList<Point>が実際にフラットなメモリを使える
- JDK 28にはfull specialized genericsはない
- コレクション、stream、APIがvalue type上でフラットかつallocation-freeになるのはfuture releaseの作業である
JDK 28にあるものとないもの
- JDK 28に入るものは次のとおり
value classとvalue recordの宣言
- JDKの既存のvalue-based classのうち、primitive wrapperのようなclassのvalue class migration
- 条件を満たすclassのスカラー化とフラット化
- より低コストなboxing
- JDK 28にないものは次のとおり
- null-restricted types
- full specialized generics
- 128ビットエンコーディング
- 完全に成熟したJEP 402
- preview featureであるため、syntaxと動作はreleaseごとにfeedbackに応じて変わる可能性がある
- JDK 28はLTSではない
- 次のLTSは2027年9月のJDK 29になる可能性が高い
- 多くの企業は安定化したValhallaをLTSで目にすることになるだろうが、JDK 28 previewが実際のコードによるfeedback loopを始動させる
エコシステムとコードに起きる変化
- 高性能Javaの領域で、Valhallaはabstractionを捨てずにdense dataを扱うための道筋になる
- データ処理、vector computation、ML、game development、finance、codecなどの領域が該当する
- frameworkやlibraryはvalue-based class migrationを始められる
- identityに依存していたコードは動作の違いを経験する可能性がある
==はvalue objectではアドレス比較ではなくsubstitutability比較になる
synchronizedはvalue objectではIdentityExceptionにつながる
Integerがvalue classになっても、ほとんどの場合binaryは引き続きlinkされる
- 新しいcompilation errorは、この種の型で同期しようとする場合である
Integerのidentityに依存した==やsynchronized(someInteger)は影響を受ける可能性がある
- early-access buildはjdk.java.net/valhallaで利用できる
よくある質問の整理
- value classはrecordとは異なる
recordはcontentをcomponentにするという選択である
valueはidentityを放棄するという選択である
- 通常のclass、record、value class、value recordの組み合わせはすべて可能である
- value objectは
==で比較できる
- 意味はアドレス比較ではなくsubstitutabilityである
- 表すデータの同等性には通常
equalsのほうが適している
- JDK 28のvalue classはnullを許容する
- non-nullable typeはfuture JEPである
- より大きなvalue classのフラット化にも重要である
- 高速なフラット
ArrayList<Point>はまだ実現していない
- type erasureのため、generic collection内のオブジェクトはヒープ上にmaterializeされる
- JDK 28でフラット化が直接機能する代表的な例は、value typeのfieldとarrayである
Point[]である
- escape analysisですべてを代替できるわけではない
- オブジェクトがfield、array、分析境界の外に出ると最適化が崩れる可能性がある
- value objectのスカラー化はより予測しやすく、method-call boundaryをさらに越えて適用できる
- Valhalla全体は複数のreleaseにまたがって拡張される
- JDK 28はvalue classの最初のpreviewである
- specialized generics、null-restricted types、128ビットエンコーディングはfuture releaseにまたがる作業である
2件のコメント
長い期間をかけてリリースされた Project Loom の仮想スレッドは便利で、JVM ランタイムレベルで多くのことを解決してくれるので、開発者が悩むことは比較的少ないですが、
Project Valhalla も、そうしたただ乗りの昼食に近い感覚で最終リリースされるといいですね
Hacker Newsのコメント
メモリの違いは本質的な部分だと言っていたが、この記事がきちんと校正されたのか疑わしい。
ついさっきまで64ビットを超える表現を持つオブジェクトはヒープ平坦化されないと説明していなかったか? 例の
Pointは32ビット整数2つに null フラグまであるので、少なくとも65ビットだ。「null フラグがあるかもしれない」という表現や、その後に続く短い強調文を見ると、AIが強調文を作っているうちに脇道にそれたように感じるし、途中の
"[IMAGE: the same Point[] array in two variants..."ブロックも残念だAIで文章作成を補助するのはいいが、自分の声が入っていないなら読む理由がない。
https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing#...
でも数段落進んだところで、LLMを通したか、それ以下のやり方で作られた記事だとはっきり分かった。
技術ブログであれ何であれ、どうかAIに代筆させないでほしい。そんな記事を読みたがる人はいない。
今日、Rustには
NonZeroU64があり、Optionalと組み合わせると1項目あたり64ビットだけで必要な動作が得られることを知った。https://doc.rust-lang.org/std/num/type.NonZeroU64.html
JEPにも明記されているとおり、これは巨大な機能の最初の成果物にすぎず、最近のJava機能がそうであるように段階的に提供されている。
当然目標はもっと大きな値も平坦化することであり、その仕組みはすでにJVM内にある。残っているのは、「ティアリングを許容する」という意図を言語レベルで表すことだ。
Valhallaに実際に投入された労力は認めるが、「モデルは強力だったが精神的に重かった」という解釈には同意しにくい。
変数が null になり得ないと言うことは精神的負担の大きい区別ではなく、特にすべてが十分に明示されているならなおさらだ。
「性能上限を犠牲にしてでもユーザーモデルを単純化する」という姿勢も、むしろユーザーに単純さを提供していた可能性がある。
プログラミング言語の型システムは、数字しか扱えないCPUの上で開発者に便利な保証を提供するためのものだ。選択的な安全保証を「複雑すぎる」という理由で減らす必要はない。
そもそも「言語モデルとJVMモデルが100%一致する必要はない」という認識にまで達していながらなおさらだ。
Javaの運営体制は心もとなく見え、最初から概ね正しい判断をしていた .NET と特に対照的だ。
最近のOracleの中でJavaに価値や関心があるのかも疑わしい。会社は今や、データセンター/コンピュート事業にレガシーな活動と莫大な負債がぶら下がっているように見える。
Oracleでまだ利益を出している部門は法務部と芝刈り部門だけではないか、と思うことさえある。
それにnull 指示子も追加予定だ: https://openjdk.org/jeps/8303099
ただ、段階的に出していく必要があるだけで、値クラス/オブジェクトを導入する今回のPRだけでもすでに20万行規模だ。
不可能だと言っているわけではなく、コップいっぱいの問題を一気には飲み込めないという話だ。
とはいえ、この片脚をかなり長いことかじっているのも確かだ。
2012年から解決済みの概念なら、状態のいくつものバリエーションを言語に直接持ち込む理由はない。レールはAに行くかBに行くかだけで、列車の積載状態に応じて分かれる。
ある概念が現れては消え、また現れて言語戦争になるなら、それは需要があるのに言語がその需要をうまく扱えていないか、扱えていても精神的な過負荷を生んでいるという合図だ。
Value,Errorstates,Null,IoExceptions,WeirdOsStatesNeededToHandleUpstairsのようなものだ。https://fsharpforfunandprofit.com/rop/
モンティ・パイソン風に言えば、そろそろ先に進もう。
Integer/intに似た参照/値投影のことだ。Valhallaチームは、各型ごとにアイデンティティありの投影となしの投影を用意する代わりに、値型はそもそもアイデンティティを持たないものにした。だから
Integerとintは同義語になる。メモリレイアウトは文脈と最適化判断に応じて自動で決まる。したがって
Integerのような基本ラッパーの==の意味も変わり、もはや「参照投影」を使っているか「値投影」を使っているかには依存しない。選択的な安全保証を「精神的に負担だ」という理由で減らしたという話では、ここではない。
Java/JVM 関連の HN コメントで繰り返し見かけるのは、驚くほど多くの人が JVM や Java の昔のイメージは持っている一方で、現在の姿はほとんど知らないという点だ。
2026 年の JVM は非常に健全な捕食者だ。欠点があるかと言えばもちろんあるが、土台はきわめて良い
仕事では最新の Java 26 とプレビュー機能、とくに
StructuredConcurrencyを使っているが素晴らしい。以前の会社で Haskell と Python を使っていた立場からしても、まったく後悔していないここ数年で出た新機能は個人的には知っているが、実務における Java は文字どおり過去に閉じ込められている
ここのコメントのかなりの部分は、今進行中の素晴らしい作業や今後出てくるさらに良い JEP と比べると、やや不公平だ。
Java を子どもにたとえるなら、最初の数年は愛情深い親である Sun に育てられ、その後ほかの子どもたちと一緒にガレージへ放り込まれ、邪悪な保護者である Oracle に放置されたようなものだ。
JDK 8 まで放置され、愛されなかったのだから、基本的にはキャッチアップを続けてきたわけだ。
「ようやく構造体や値型のようなものが入った」というのは事実だが、それは巨大で官僚的かつ敵対的な企業プロセスのせいで成長が阻害されていたからだ。今では自由になり、OpenJDK ファミリーを通じて愛されている。
これからも 一度書けばどこへでもデプロイできる楽しさを味わい続けるだろう
愛情深い親が育てていたが、資金難のため里親に預けられ、そこで放置されたというほうが近い。
その後、新しく愛情深い親である Oracle が養子に迎え、Java は花開いて健全で安定した大人になった。
OpenJDK を参照実装にしてプラットフォームのオープンソース化を完了したのも Oracle であり、以前は独占的だった JFR や Mission Control のようなツールもオープンソース化した。
言語チームの創設メンバーも多く維持されており、こうした買収ではかなり珍しいことで、Java は言語とランタイムの両方で大きく改善された
Oracle は下位互換性の大半を維持しながら、前例のないペースで Java を前進させた。
.NET は「最初から正しくやった」と言われるが、それが .NET Framework/.NET Core/.NET への分離と書き直しを指すなら、この文脈でも筋が通らない。.NET は Java を見て学べたはずなのに、失敗した部分がある。
MySQL も同じだ。このサイトでは「死んだ」と言われていたが、本当に知っている人から見れば Oracle の下で息を吹き返した
Sun 配下での最後の Java バージョンは 2006 年に出ており、Oracle が Sun を買収したのは 2010 年、JDK 7 は 2011 年、JDK 8 は 2014 年に出た。
チームはおおむねそのままで、最大の違いは Oracle が放置を終わらせ、より多くの資金を投入したことだ。そのため買収後に Java のペースは速くなった。
「キャッチアップ」と言うが、誰に追いつくというのかも曖昧だ。Java と同程度かそれ以上に人気のある言語は JS/TS と Python くらいしかない。
Java が遅れていると言う人は、たいてい Java よりはるかにうまくいっていない言語と比較している。特定の機能を好む人たちは、その機能を持つ言語がその機能のおかげではなく、それにもかかわらず低迷していることを見落としがちだ。
管理者は素早いリリースを好む一方で、むしろ Sun 時代からいる技術リーダーたちは慎重に、ゆっくりでも正しくやるべきだと主張している。
Java が 2003 年ほど人気ではないという空気感は理解できるが、あの時期は Java だけでなくソフトウェア生態系全体にとっても例外的に統合されていた時代であり、その前後であれほど統合されたことはない
今や本当に 一度書いてどこへでもデプロイできる技術は WebAssembly だ。JVM の順番はあったし、負けた
それでも Java が「成長阻害」されたとまでは呼ばない。選択を行い、その一部は合理的で一部はそうではなく、そうした選択は後から修正するのが非常に難しい。
C++ を見ても、C との半互換性は個人的には修正不能な 150 フィート級のアホウドリだと思っていて、C++11 以降の多くのバージョンはそのアホウドリを少しだけ我慢しやすくする作業だった。
JVM ですべての値クラスを基本型のような単一の L-タイプとして扱うのは、難しい問題に対するかなりクリーンな解決策だと思う。
結局これらすべては、Java 2 が下位互換性のためにジェネリクスを型消去で実装することに決めたところから続いており、C3 はその結果を見てその道を拒んだ
Java の 値型の進化だけでも技術スリラーを 1 冊書けそうだ。
メーリングリストを読み、関連動画をすべて見たが、設計を常に Java らしく見える何かへと統合していく過程は本当に印象的だった。
同時に、値型が何を意味するのか、どの最適化をどこで行えるのかを、はるかに細かく掘り下げていた
valueを 1 つ追加するだけだ値クラスでは
==は事実上memcmp()のように動作することになるわけだが、これは少し残念で、カプセル化を壊して実装詳細を露出させるためだ。
クライアントコードは、与えられた値が内部的にどう表現されているかに応じて分岐できてしまう。ある意味では、少なくとも内部状態は露出しない識別性比較よりも悪い
古典的オブジェクト指向を新しいやり方でやるのではなく、オブジェクト指向の理念から生まれた言語が、脱オブジェクト指向の世界へさらに一歩進むやり方だ
Java側でも比較からパディングを除外したり、パディングバイトを 0 に強制したりする程度のことは十分考えたはずだ。
これは文字列にも適用できなければならない。文字列は明らかに引き続きヒープに割り当てられるだろうし、新しい「構造体」の中のポインタを
memcmpするのは、まさに識別性比較だJava はオブジェクトの識別性確認と等価性確認を分離している。
==は基本的に 2 つのポインタが同じかを見るだけで、等価性はequals/hashCodeのようなインターフェースに基づく主観的な概念だ。そのため
new Integer(1000) == new Integer(1000)は以前はfalseだったが、今後はtrueになり、new Integer(1000).equals(new Integer(1000))はtrueであり、new Integer(10) == new Long(10)は以前はfalseだったが、今後はコンパイルエラーになる。以前の Java では、ある値以下の整数が正規化された型に置き換えられることがあり、たしか 128 前後だった記憶がある。だから 10 と 1000 の差が生じていた。
今では上の比較は暗黙にアンボクシングされるようだ。
Integer/Long比較が以前はfalseで、今後はコンパイルエラーになるのを見ると、アンボクシングが関与しているのは明らかだ。変数経由なら、まだ以前の動作を得られるかもしれない。
とにかく、値クラスが識別性を失うと
==はポインタ等価からビット単位の等価へ変わる。こうしたさまざまな隅のケースが解決されることを望むが、技術的には破壊的変更だ値クラスの趣旨は理解できるが、実装には欠陥がある。
次のコードは何を出力するだろうか。
Point a = new Point(10, 10); Point b = a; a.x = 100; System.out.println(b.x);これまでは答えは明確だったが、値クラスが追加されると、答えは
Pointが値クラスか参照クラスかによって変わる。したがって、この設計は 可読性 を損なう。これは一様性の原則への違反だ。Weinberg の『The Psychology of Computer Programming』では、一様性とは、見た目が似ているものは似たように動作し、違って見えるものは違って動作するとユーザーが期待するという心理的原則だと説明している。
プログラミング言語が、使用箇所ではほとんど同じに見える 2 つの構文に意味的に異なる動作を許すと、読み手の認知負荷が高くなる。代入、等価性、識別性、変更が通常の参照オブジェクトのように動くのか、値のように動くのかを知るために、型宣言を確認したりツールに頼ったりしなければならない。
宣言時だけでなく使用時にも
valueキーワードを要求していれば修正できたはずだ。たとえばvalue Point a = new Point(10, 10);のように書く形だhttps://openjdk.org/jeps/401
finalである。もちろん、その 4 行だけ見てもそういうことが起きると分かるはずはないが、この問題は今でもある。
Pointがレコードでも同じことが起きるa.x = 100;という文は有効ではないはずだ。レコード型は不変だからだ。したがって、懸念している状況は起こりえないはずだ
value Point a = new Point(10, 10); value Point b copy= a; a.x uniq= 100; System.out.println(b.x);のように書ければ、複製/コピーが起きることや、フィールド変更が他のオブジェクトのフィールドに影響しないことがずっと明確になるJava の世界では .NET の存在を認めるのは失礼だと分かっているが、これが .NET の struct とどう違うのか気になる。
値型、ジェネリクスの特殊化、ボクシングをざっと見る限り、同じ選択をしているように見える
C# が低レベルの観点から C をほぼそのままコピーしたのに対し、Java 側は高レベルからアプローチし、どの制約がどの利点をもたらすのかを詳しく分析している。
他の言語では struct/class の分類は二分法的だが、Java は基盤ドメインの意味を反映して、より細かな制御を可能にしている。
そして struct には、特に並行コンテキストでさまざまな 自分の足を撃つ系の罠 があることが分かっている
個人的には、C/C# の struct は変更可能でコピーで渡されるが、値クラスは変更できず、値として渡されると見ている。
Java ではスタック割り当てはできないと思う
「C# の struct は識別性と変更を持つので、代入や受け渡し時のコピー意味論を正確に定義しなければならず、その結果プログラマにとってはより重いモデルとなり、ランタイムにとっては自由が少なくなる」といった偽の二分法は、説明されている内容とうまく一致しない。
Java のクラス参照意味論における識別性はないとしても、特定アドレス上の固有のメモリ構造という意味では、当然ながら依然として識別性はある。これは Java 用語に関する言葉尻を捉えているのに近い
脚注 6 の「C# の struct とどう違うのか」は不正確だ。
記事に AI 生成画像 が大量に入っているのを見ると、執筆や少なくとも調査過程にもかなり幻覚が混じっていたのではないかと思ってしまう
文章はややぼんやりしていて大げさですが、幸い原文の文書はかなり読みやすいです
最上位ページ: https://openjdk.org/projects/jdk/28/spec/
JEPの状態: https://bugs.openjdk.org/secure/Dashboard.jspa?selectPageId=...
C#、Swift、Java、Rust における関連する発展を誰かが追跡してくれるといいのですが。どれもハードウェアに追いつくために競い合ってきていて、互いに影響し合っていると思います
個人的には、これらの変化が FFIメモリ共有 にどんな影響を与えるのかが気になります