Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

http://cr.openjdk.java.net/~briangoetz/valhalla/sov/02-object-model.html

State of Valhalla

セクション2:言語モデル

Brian Goetz, Dec 2019

このドキュメントは、インライン型を組み込むための言語モデルについて説明します。 インライン型のJVMモデル、およびJavaソースコードからJavaクラスファイルへの変換戦略については、別のドキュメントで説明する予定です。 (このドキュメントでは、「現在」という言葉を、この言語の今の状況、すなわちインライン型がない状態を指すものとして使います。)

今の我々の位置

現在、型はプリミティブ型と参照型に分けられています。 8つの組み込みプリミティブ型があります(voidは型ではありません)。 参照型とは、プリミティブ型ではない型で、クラスまたはインターフェースとして宣言された型と、 配列型(String[], int[])および参照型のパラメーター化(List<String>, List<?>)などの非宣言型を含んでいます。

参照型とプリミティブ型は、考えられるほとんどすべての点で異なります。 参照型はメンバー(メソッドとフィールド)とスーパータイプ(スーパークラスとインターフェース)を持ち、すべてが(直接または間接的に)Objectを拡張します。 プリミティブ型にはメンバーがなく、型システム上の「孤島」であり、スーパータイプやサブタイプはありません。 プリミティブ型を参照型に接続するために、各プリミティブ型はラッパー型に関連付けられています(Integerintのラッパー型です)。 ラッパー型は参照型であるため、メンバーを持つことができ、サブタイピングに参加できます。 プリミティブ型とそれに対応するラッパー型の間には、ボクシングアンボクシングの変換があります。

Types, current world

値集合

すべての型は値集合を持ちます。これは、その型の変数に格納できる値の集合です。 (たとえば、intのようなプリミティブの値集合は32ビット整数の集合です。) 型Tの値集合を表すために、Vals(T) と記述することにします。 型Tが型Uのサブタイプである場合、Vals(T) ⊆ Vals(U) です。

オブジェクト はクラスのインスタンスです。 現在、すべてのオブジェクトには一意の オブジェクトアイデンティティ があります。 参照型の値集合は オブジェクト ではなく、 オブジェクトへの参照 で構成されます。 String型の変数がとりうる値は、 Stringオブジェクト自体ではなく、それらのStringオブジェクトへの参照です。 (経験豊富なJava開発者でさえ、オブジェクトを直接保存、操作、またはアクセスできないことに驚くかもしれません。 私たちはオブジェクト参照を扱うことに慣れすぎているので、違いに気付かないことさえあります。 実際、Javaオブジェクトが値渡しされるのか参照渡しされるのか、というのはよくある「落とし穴」の質問であり、答えは「どちらでもない」です。 オブジェクト参照値渡しされます。)

プリミティブ型の値集合は、プリミティブ値で構成されます(nullは含まれません)。
参照型の値集合は、オブジェクトインスタンスへの参照、またはnullで構成されます。

前の段落の2つの重要な事実(すべてのオブジェクトには一意のアイデンティティがあり、オブジェクトを操作する唯一の方法は参照を介すること)は両方とも、インライン型を取り込むと変わります。

次の図では、Javaプログラムの変数に格納できる値を強調表示するために、表現可能な値を赤いボックスで示します。

Values, current world

現在の世界における値の宇宙は、プリミティブ値とオブジェクトへの参照で構成されています。

現在の世界を要約すると、以下の通りです。

  • 型は、プリミティブ型と参照型に分かれています。
  • 参照型は、プリミティブ型ではなく、宣言されたクラス、宣言されたインターフェース、および配列型を含みます。
  • プリミティブには対応するラッパー型があり、そしてそれは参照型であり、プリミティブ型とそれに対応するラッパーの間にはボクシングおよびアンボクシングの変換があります。
  • プリミティブ型の値集合にnullが含まれることはありません。
  • 参照型の値集合はオブジェクトではなくオブジェクトへの 参照 で構成され、常にnullが含まれます。
  • オブジェクトにはオブジェクトアイデンティティがあります。

インラインクラス

準備が整いましたので、言語の型システムで インラインクラス に対応する方法に取り組めます。 インラインクラスのモットーは次のとおりです。 クラスのようにコードを書き、intのように機能します。 このモットーの後半部分は、インライン型がこれまでに概説したプリミティブ型の動作と整合する必要があることを意味します。 (実際、インライン型の傘下にプリミティブ型を含めたいと思っています;すでに二部に分かれた型システムをより多くのカテゴリに分割することは望ましくありません。)

インラインクラスはクラスのようにコードを書くため、クラスが持つことができるほとんどのものを持つことができます:フィールド、メソッド、コンストラクター、スーパーインターフェース、型変数などです。

inline class Point {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }
}

Valhallaがもたらす最初の大きな違いは、インラインクラスのインスタンス(これを インラインオブジェクト と呼びます)にアイデンティティがないことです。 これは、アイデンティティに依存した特定の操作(同期など)がインラインオブジェクトでは許可されないことを意味します。 混乱を避けるために、従来のクラスを アイデンティティクラス と呼び、それらのインスタンスを アイデンティティオブジェクト と呼びます。

インラインクラスのインスタンスはオブジェクトですが、アイデンティティはありません。

オブジェクトアイデンティティは、とりわけ、 可変性レイアウトのポリモーフィズム を可能にします 。 アイデンティティを放棄することにより、インラインクラスはこれらのことを放棄しなければなりません。 したがって、インラインクラスは暗黙的に final で、 Object 以外のクラスを拡張できません(ただし、インターフェースは実装できます)。 また、それらのフィールドは暗黙的に final です。 さらに、インラインクラス V の表現には、V 型のフィールドを直接または間接的に含めることはできません。 (さらに、インラインクラスは clone() メソッドも finalize() メソッドもオーバーライドできません。)

型をプリミティブ型と参照型に分割する代わりに、Valhallaはそれらを インライン 型と参照型に分割します。 インライン型にはプリミティブが含まれます。 「参照型」の意味は固定されたままです。 参照型は、インライン型ではないものです(宣言されたアイデンティティクラス、宣言されたインターフェース、配列型など)。 型についての図を更新して、インライン型を含めるようにしてみましょう。

Types, Valhalla

値集合

値集合がオブジェクトインスタンスへの 参照 で構成されるアイデンティティクラス(または null )とは異なり、インラインクラス型の値集合は、そのクラスの可能な インスタンス の集合です(プリミティブと同様に、インラインクラスは null不可 です)。 インラインオブジェクトは、現在のプリミティブと同様に直接表現されます。このことを値集合の図に反映しましょう。

Values, Valhalla

プリミティブと同様に、インラインクラスの値集合は、オブジェクト参照ではなく、そのクラスのインスタンスの集合です。

すべての型は デフォルト値 を持ちます。 プリミティブ型の場合、デフォルト値はある種のゼロ(0, 0.0, false など)です。 参照型の場合、デフォルト値はnullです。 インラインクラスの場合、デフォルト値はその型のインスタンスのうち、すべてのフィールドがそれぞれの型のデフォルト値を取るものです。 クラス型 C の場合、 C のデフォルト値を C.default と記載するでしょう。 (ジェネリックなコードでは、 T.default と記載するでしょう。型消去によるジェネリクスの場合、 T は常に参照型であるため、これは null と評価されます。 ジェネリクスが特殊化される場合は、T.default も特殊化されます。)

新しいトップインターフェース

コンパイル時と実行時にインラインクラスとアイデンティティクラスを区別するために、 制約付きインターフェースのペアであるIdentityObjectInlineObjectを導入します。 インラインクラスは暗黙的にInlineObjectを実装します。アイデンティティクラスは暗黙的にIdentityObjectを実装します。(両方を実装しようとするとエラーになります。) これにより、アイデンティティに依存する操作を実行する前にオブジェクトのアイデンティティを動的にテストするコードを作成できます。

if (x instanceof IdentityObject)) {
    synchronized(x) { ... }
}

型(およびジェネリック型の境界)でのアイデンティティ要件を静的に反映することもできます。

static void runWithLock(IdentityObject lock, Runnable r) {
    synchronized (lock) {
        r.run();
    }
}

等値性

現在の世界では、プリミティブ型ごとに == が定義されており、参照型では、2つの値が == となるのは、両方がnullであるか、同じオブジェクトへの参照である場合です。 現在、すべての参照はアイデンティティを持つオブジェクトを指しているため、オブジェクトアイデンティティを使用して「同じオブジェクト」を定義できます。

コンポジションを用いて == をインラインオブジェクトに拡張できます。 2つのインラインオブジェクトが == となるのは、2つが同じ型で、それぞれのフィールドについて、全てのペアがフィールドの静的型の == に従って等しい場合です ( float および double を除きます。これらは、 Float::equals および Double::equals のセマンティクスに従って比較されます)。 この定義では、2つのインラインオブジェクトが 置換可能 、すなわち違いが識別できない場合にのみ等値であると言います。

配列

インターフェース I を実装する任意のクラス X (インラインまたはアイデンティティ)の場合、 X の配列に対して次のサブタイプ関係が保持されます。

X[] <: I[] <: Object[]

アイデンティティに依存した操作

現在、いくつかの操作はオブジェクトアイデンティティの観点から定義されています。 これらの一部は、すべてのオブジェクトインスタンスを対象とするように適切に拡張できます。 残りの操作は部分的になります。 これらには以下が含まれます。

  • 等値性。 Object 上で == を全域化します。 つまり、現在において意味がある場合、新しい定義はその意味と一致します。 (次のセクションで説明するように、インラインオブジェクト への参照 に関連する追加の作業がいくつかあります。)
  • System::identityHashCode。 identityHashCode の主な用途は、IdentityHashMapなどのデータ構造の実装における、 == の前のフィルターとしてです。 identityHashCodeは、等値性を全域化するのと同じ方法で全域化できます。つまり、すべてのフィールドのハッシュからハッシュを導出します。
  • 同期。 これは部分的な操作になります。 同期が実行時に失敗することを静的に検出できる場合(インラインクラスでsynchronizedメソッドを宣言するなど)、コンパイルエラーを発行できます。 そうでない場合、インラインインスタンスをロックしようとすると、実行時に IllegalMonitorStateException となります。これが正当化できるのは、対象オブジェクトのロックプロトコルを明確に理解していないのにそれをロックするというのは本質的に不注意によるものだからです。任意の Object またはインターフェースインスタンスをロックするというのは、まさにそういうことなのです。
  • Object::wait および Object::notify。 同期と同じです。
  • 弱い参照。 これについてはどちらにすることもできます。Objectの弱参照を作成することを部分化もできますが、これは厄介な結果になります:弱参照はほとんど役に立たなくなります。なぜなら、何らかの弱いデータ構造を維持したいすべてのクラスは、アイデンティティオブジェクトとインラインオブジェクトの別々のパスに分岐する必要が出てくるためです (これはidentityHashCodeの部分化に似ています)。一方、単純な動作(インラインオブジェクトを保持する弱参照は決して消去されないなど)を選択すると、初めは存在していた、弱参照を使用する目的となるGCフレンドリーな振る舞いの一部が失われます。この件はさらなる分析が必要です。

インライン型と参照型

これまでのところ、「プログラム可能なプリミティブ」に非常によく似たインライン型を構築してきました。 しかし、プリミティブの最大の欠点は、静的にも動的にも、プリミティブとオブジェクトの間ではっきり分かれている点です。 「プログラム可能な」部分は、インライン型がメンバーとスーパータイプを持つことができるという点で、いくつかのギャップを狭めます。 しかし、このギャップをさらに狭くしたいと思います。

現在の世界では、 ボクシング変換を介してプリミティブ型から参照型に変換しています。 これは、より多くの多態的なコードを書くことができるので便利です。 サブタイプまたはボクシングを介して、任意の値を Object で表すことができます。 しかし、ボクシングにはいくつかの重大な欠点があります。 ボックス型は、特注の手作りクラスであり、プリミティブ型への言語的接続は限られています - これは、インラインクラスに確実に対応しません。 さらに悪いことに、結果のボックスには「偶発的な」オブジェクトアイデンティティがあり、多くのVM最適化の妨げになります。 「ボクシングは遅い」という信念は、この偶然のアイデンティティに由来するものです。

私たちがやりたいのは、実行時のアドホックで軽量な方法でインライン型の世界を参照型の世界に接続することです。

ボクシングは死んだ、インライン拡大よ栄えよ

インラインクラスの値集合はオブジェクトインスタンスで構成されますが、アイデンティティクラスの値集合はオブジェクトインスタンス への参照 で構成されます。 この表現の違いは、現在の世界のプリミティブとオブジェクトの違いの主な原因の1つです。

インラインクラスを型システムの残りの部分に接続し、インターフェースを実装して Object を拡張できるようにします。 しかし、インターフェース II を実装するアイデンティティクラス C 、および I を実装するインラインクラス V があるとします。 I の値集合は I ですか? 明らかに、 CV の両方の値集合を含める必要がありますが、これらの集合は構造的にまったく異なります。 一方にはオブジェクトが含まれ、もう一方にはオブジェクトへの参照が含まれます。 これは、橋渡しする必要があるオブジェクトとプリミティブの分断です。 現在の世界は、これを不器用にボクシングで埋めています。 より均一で軽量な方法で橋渡しをしたいと考えています。

インターフェース(および Object )は参照型です。つまり、値集合はオブジェクト参照で構成されている必要があります。 それが、Valhallaの次の大きな違いにつながります。 すなわち、アイデンティティオブジェクトを操作できるのは参照を介してのみですが、インラインオブジェクトでは、直接またはオブジェクト参照を介して、 どちらの方法でも 操作および保存できます。

Valhallaの値の宇宙は、プリミティブ値、インラインオブジェクト、およびアイデンティティオブジェクトとインラインオブジェクトの両方への参照で構成されています。

Valhallaでは、 インライン拡大変換 を介してインラインから参照型に変換します。 これはボクシングと似ていますが、大きな違いがあります: 変換の結果は(ボックスのように)アイデンティティオブジェクトではなく、 インラインオブジェクトへの参照 です。 (結果の ObjectObject::getClass を呼び出すと、ボックス型ではなく、元のインラインオブジェクトのクラスが報告されます。) これにより、VMの最適化能力を損なうことなく、インライン型と参照型の間で必要な相互運用が可能になります。

インライン拡大変換は、ボクシングのパフォーマンスの欠点をほとんど伴わずに、ボクシングの望ましいセマンティクスを提供します。

演算子 ref v は、 v がインラインオブジェクトの場合は v への参照として定義し、 v が既にオブジェクト参照である場合は v 自体として定義すると便利です。 そうすれば、 ref はすべての表現可能な値の全域で定義され、常に参照を返します。 (逆の演算子 unref は部分的であり、インラインオブジェクトへの参照にのみ適用され、2つは 射影と埋め込みのペア を形成します。)

インライン拡大変換は、インライン型からそれが実装する任意のインターフェース、または Object に存在し、 ref 演算子の適用として定義されています。 これにより、 I の値集合に関する質問に答えることができます。 これには、 C のすべてのインスタンスへの参照と、 V のすべてのインスタンスへの参照が含まれます。

インターフェース(および Object )の値集合は、値 null に加えて、アイデンティティまたはインラインオブジェクトのいずれかであるオブジェクトへの参照で構成されます。 インターフェースまたは Object インスタンスでアイデンティティに依存する操作を実行すると、実行時に失敗する場合があります。

これはボクシングに目新しい名前を付けただけでは?

この時点で、読者は単に用語のトリックを演じただけ、含意のある用語「ボクシング」をまだ含意のない用語「インライン拡大」に置き換えただけではないかと疑問に思うかもしれません。 実施したのが単に名前の変更だけなのであれば、それは確かにトリックになります。 インライン拡大が単に名前変更されたボクシングではない理由は、Valhalla JVMでは、インライン拡大および縮小変換が、対応するボクシングおよびアンボクシング変換よりもはるかに軽いためです。 ボクシングの問題は、アドホック(訳注:プリミティブという限られた型のみに定義されていること)であり、コストがかかることです。 私たちはこれらの両方の懸念に対処しようとしています。

参照射影

特定のインラインクラスのオブジェクトへの 参照 の集合とnullを記述することができると便利な場合がよくあります。 インライン型 V が与えられた場合、値集合が以下によって与えられる型 I が必要です。

ValSet(I) = {null} ∪ {v ∈ ValSet(V) : ref v}

このようなインターフェース IV参照射影 と呼びます。 単一のインラインサブタイプのみを許可するシールされたインターフェースを使用して、参照射影を簡単に構築できます。

sealed interface I permits V { }
inline class V implements I { }

参照射影は、現在の世界でラッパークラスが果たす役割を果たします。 しかし、すべてのインラインクラスに手書きのアドホックラッパーを持たせたくはありません。 インラインクラスから参照射影を機械的に導出し、それを参照するための統一された方法を用意したいと考えています。 この方法では、インラインクラスを参照射影にマッピングするメンタルディクショナリを維持する必要はありません。 任意の型 T について、 T.refT の参照射影を示すようにします。

インラインクラス V が上記の要件を満たすインターフェース I を1つだけ実装し( V のみを許可するようにシールされ)、IV の宣言されたすべてのスーパーインターフェースを実装する場合、 IV の参照射影 である と呼び、V.refI のエイリアスとします。 V がそのようなインターフェースをただ1つ実装しているのでなければ、合成インターフェースが暗黙的に作成され、 V.ref はそのインターフェースのエイリアスになります。 V のスーパーインターフェースとパブリックメソッドは合成インターフェースに持ち上げられます。

参照射影はインラインクラスのスーパーインターフェースの1つであるため、 V から V.ref へのインライン拡大変換が既にあります。 それに加え、 V.ref から V への インライン縮小変換 を定義します。 この変換は、 unref 演算子を適用するもので、null に対しては NullPointerException をスローします。

このペアの変換では、インラインクラスは、プリミティブ型が従来からラッパー型で行っていた参照射影と同じ関係を持ちます。 ボクシング変換(オートボクシング、条件の入力、オーバーロード選択)の観点から定義されている既存のルールを簡単に拡張して、インラインの拡大および縮小変換を組み込むこともできます。 その結果、これらの変換に関する既存のユーザーの直感は、新しい世界では変わらずに引き継がれますが、インライン拡大では現在のように「偶発的な」アイデンティティを持つオブジェクトが作成されないため、ボクシングの実行コストはかかりません。

参照型 R は、それ自体の参照射影となるためのすべての要件をすでに満たしているため、参照型 R については、 R.refR 自体のエイリアスとします。 これで、すべての型 T について、 T.ref で示される参照射影を確実に持つようにできました。

インターフェース

歴史的には、クラスがインターフェースを実装することはいくつかのことを意味していました。

  • 適合性。 クラスには、メンバーとして、インターフェースのすべてのメンバーがあります。
  • 推移性。 このクラスのサブクラスもインターフェースを実装します。
  • サブタイピング。 クラス型は、インターフェース型のサブタイプです。

インラインクラスをサポートするために、この最後の箇条書きであるサブタイピングを少し改良する必要があります。 クラス型の 参照射影 は、インターフェース型のサブタイプであると言います。 (アイデンティティクラス型は、自分自身の参照射影です。)

オブジェクト

Object には、すべてのクラス、インライン、およびアイデンティティのルート型としての役割があるため、インターフェースと多くの特性を共有しています。 既に述べたように、すべてのインライン型から Object へのインライン拡大変換があります。

ただし、 Object は具象クラスであるため、残念ながら、コンストラクターを介して Object 直接インスタンス化することは可能です。 インターフェースは継承されるため、 ObjectInlineObjectIdentityObject も実装できません。 ですが、 new Object() によるインスタンス化の結果はアイデンティティ型のインスタンスである必要があります(他の理由で Object をインスタンス化するポイントがないため)。

この罠からはいずり出るには、手の込んだ動きが必要です。 まずは IdentityObject を返す静的ファクトリー Object::newIdentity を作成し、 それから、さまざまなツール(コンパイラー警告やJITのマジックなど)を使って、既存のソースおよびバイナリーでの new Object() の使用をそちらに移行しようとすることから始めましょう。 そして最終的には、直接インスタンス化するための Object コンストラクターを( protected にしてしまうことで)「廃止」するのです。

等値性、ふたたび

インラインオブジェクトの == 拡張は完了していません。 インラインオブジェクト自体に対しては定義しましたが、参照型で、値集合の中にインラインオブジェクトへの参照を持つ可能性のあるものについてはまだ定義していません。 これについて、2つのオブジェクト参照が等しいとは、両方ともnullであるか、同じアイデンティティオブジェクトへの参照であるか、または == が成立する2つのインラインオブジェクトへの参照である場合とします。 これにより、 ==代入可能性 のセマンティクスがすべての値に拡張されます。 すなわち、2つの値は、 NaN が持つ従来の動作を除いて、どのような方法でも区別できない場合にのみ == です。

これにより、 == について、次の有用な不変式( NaN の従来の動作を除くすべてで成り立つ)が得られます。

  • == は反射的、すなわちすべての v に対して v == v です。
  • 2つのインライン値が参照に拡大されるとき、元の値が == だった場合にのみ、結果も == となります。
  • 2つの参照がインラインオブジェクトに縮小されるとき、元の参照が == だった場合にのみ、結果も == となります。

Object::equals の基本実装は、 == に委任します。 Object::equals を明示的にオーバーライドしないインラインクラスでは、それがデフォルトです。 (同様に、 Object::hashCode の基本実装は System::identityHashCode に委任します。これもデフォルトです。)

参照射影が必要な理由

参照射影の定義は意味があり、参照型との関係についての既存の直観と一致していますが、これらの型が非常に重要である 理由 をまだ十分に動機付けていません。

インライン型を使用できない場合は次のようにいくつかあります。

  • 無効値。 Nullはインライン型の値ではなく、すべての参照型の値ですが、「V型のnull値を許可する値」という概念を表現したい場合もあります。
  • 非平坦化。 インライン値はオブジェクトと配列に規則正しくフラット化されます。 通常、これは私たちが望むものです。 ただし、場合によっては、メモリー使用率をより細かく制御する必要がある場合があります。たとえば、「幅の広い」インラインクラス(多くのフィールドを持つクラス)があり、それらの疎な配列が必要な場合などです - 平坦化された値の配列よりも、参照の配列の方がメモリーを効率的に使える場合があります。
  • 再帰表現。 インラインクラスは、表現について自分自身に再帰的に依存することはできません。 通常、これは重大な制限ではありませんが、参照型でこれをシミュレートしたい場合もあります(たとえば、「次の」フィールドを持つ Node クラス)。
  • 型消去によるジェネリクス。 既存の型消去ジェネリクスは、型パラメーターが参照型であることを前提としています - アイデンティティ依存の操作や、null値などが許可されます。特殊化されたジェネリクスが使えるようになって、インライン型と型消去ジェネリクスの相互運用ができるようになるまで、型パラメーターには参照型を使用したいのです。
  • 妥当なゼロがない場合。 インライン型のデフォルト値は、すべてのフィールドがデフォルト値をとる値です。 一部の型では、この値は妥当です( Point のデフォルト値として (0, 0) は妥当です)が、場合によっては( Rational など)、この値は無意味か、または危険ですらあります。 そういった場合は、初期化されていない値を表すために null を使用できるように、参照型を使用したいこともあります。

これらすべての状況で、インライン型の代わりに参照型を使用できます。 ただし、 Object などの広い参照型を使用すると、多くの損失が発生します - 型安全性が失われ、クライアントはインラインの世界に戻るためにコードに未チェックのキャストを挿入する必要があります。 インライン型に参照射影を使用すれば、インラインの拡大と縮小の変換によって、値集合が特定のインラインクラスの値集合と密接に結び付けられた参照型があり、明示的な変換を使用せずにそのインラインクラスに自由に変換できます – これにより、私たちが望む型の安全性と利便性が救われます。

例として、特殊なインスタンス化をサポートするために Map インターフェースを移行する問題を考えてください。 Map::get メソッドは、要求されたキーがマップに存在しない場合は null を返しますが、 MapV パラメーターがインライン型の場合、 null は値集合のメンバーではありません。 get() メソッドを以下のように宣言すれば、これを捕捉できます。

public V.ref get(K key);

これは、 Map::get の戻り値の型が V への参照か、または null 参照のどちらかになるという概念を捉えています。

移行

これまでに説明した手法は新しいコードには十分ですが、準備しておきたい移行シナリオがいくつかあります。

移行: 値ベースのクラス

値ベースのクラスがすでにいくつも存在していて、それらはインラインクラスへのスムーズな移行を可能にするために設計された一連の制限を満たしています。 そのような例の1つに java.util.Optional があります。これは、Java 8にインラインクラスがあったとしたならインラインクラスとして宣言できたはずです。 Optional をインラインクラスに移行して、インラインクラスの実行時の利点を活用したいと思います。 ( java.util.time パッケージのクラスも同様の状況にあります。)

既存のクライアントには、変数の型、メソッドのパラメーターまたは戻り値の型、型パラメーターなどで、 Optional 型が多数使用されています。 これらのクライアントはすべて、 Optional が参照型であると想定しており、参照型はNULL可能です。 したがって、 Optional を直接インライン型に移行することはできません。

次善策は、 Optional を何らかのインラインクラス(場合によってはプライベートなクラス)の 参照射影 として定義することです。 つまり、 Optional をインターフェースに移行し(これを互換性のある移行にするために少しJVMの作業を行う必要があります)、それをシールしてインラインクラスを1つだけ許可するように手配します。 T.ref との対称性のために、このインラインクラスを Optional.inline と呼ぶことができます(実際に宣言されている方法に関係なく)。

この移行後、 Optionalインスタンス はアイデンティティクラスではなくインラインクラスになりますが、既存のコードは引き続き、参照を介して Optional 値を格納したり渡したりします。 それらを直接表現することが実際に違いを生むのは、それらがヒープに出会う場所、つまりフィールドと配列要素です。 そしてそこでは、 Optional のそれら特定の用途を Optional.inline に自由にかつ段階的に移行できます。 インラインの拡大と縮小の変換によって、フィールドまたは配列を Optional から Optional.inline に変更するのは、ソース互換の変更になります。 既存のAPIは、参照射影である Optional を引き続き使用する可能性が高いでしょう。

このような移行から生じる可能性のある非互換モードが1つあります - クライアントが getClass() の結果と M.class を比較する場合です。 移行前は、 Optional インスタンスは Optional.class クラスを持っています。 Optional がインターフェースに移行されると、 Optional インスタンスは getClass で照会されたときに実装クラスを報告するでしょう。

M.class の使用を取り巻くこの非互換性は、ここで概説する移行アプローチの主要な互換性コストです。これは、 getClass() の結果とインターフェースのクラスリテラルとを == で比較するときにコンパイル警告を発行することで多少緩和できます。実行時に失敗することがわかっているためです。

移行: プリミティブ

インライン型にプリミティブの抽象化という役割を割り当てましたが、実際にプリミティブをインライン型に包含できるようにするために、さらに作業が必要です。

Optional の移行と同じ手法を使用して、ラッパー型 Integer とその仲間たちをインターフェースに移行することから始めます。 ( Number については、追加のトリッキーな部分がいくつかあり、対処する必要があります。) これらのインターフェースは、プリミティブの参照射影になります。 これを行うには、最初にプリミティブラッパーのアイデンティティへの依存からユーザーを引き離す必要があります。 これは、パブリックコンストラクターを削除する(実際に削除するのではなくプライベートになる)ために非推奨にし、一定の移行期間中は Integer インスタンスを「永続的にロック」します(こちらのアプローチについてはJEP 169を参照してください)。

この動きは、プリミティブラッパーのアイデンティティに依存するコードを壊すリスクがあります。代入可能性を持った == の定義を考えれば、 == による比較はほぼ問題になりませんが、ラッパーをロックするコードは実行時に失敗するでしょう。

その後、プリミティブ型をインラインクラスとして明示的に宣言できます。

inline class int implements Integer { /* implementation */ }

自己参照については明らかに多少の調整を行う必要がありますが、今や、 int にスーパーインターフェースとインスタンスメソッドを追加するのも自由ですし、通常はクラスのように扱うことができます。 参照射影 int.refInteger のエイリアスになっていて、 Integer.inlineint のエイリアスになっています。 2つの型の関係は、移行された値ベースのクラスの場合と同じです。 プリミティブが真のインライン型である場合でも、配列の間には引き続き以下のサブタイピング関係があります。

int[] <: Integer[] <: Object[]

これで、 intInteger との間で2つの変換が定義されました。 既存のボクシング変換と、新しいインライン縮小および拡大変換です。これらを調整する必要があります。 しかし、便利なことに、ラッパークラスの偶発的なアイデンティティ(非推奨です)を除き、これらは同じセマンティクスを持つように定義しています。 ボクシングは、ランタイムが規則正しい最適化によって除去することがより単純になりますが、その一方で、変換や、オーバーロード選択や、推論にまつわるすべての既存ルールは維持されます。 同様に、オーバーロード選択でのボクシングの役割を一般化して、インラインの拡大と縮小を代わりに使用できるため、この移行ではオーバーロード選択の決定方法は変更されません。

移行: 特殊化されたジェネリクス

ジェネリクスは現在、単一の参照に型消去されます。 いずれはジェネリクスがその表現を特殊化できるようにして、 ArrayList<Point> のようなジェネリック型も Point[] がもたらす平坦化と密度の利点を得ることができるようにしたいと思っています。 これには追加の時間がかかり、移行互換性の問題がさらに発生します。 ArrayList を特殊化可能な型に移行しても、既存のクラスファイルは 型消去による ArrayList への参照が多く残されたままでしょうから、これらは引き続き動作する必要があります。

将来の機能として、特殊化されたジェネリクスの余地を残そうとしています。 現在の型消去されたジェネリクスは、参照型でのみ機能し続ける可能性があります。 List<int>Optional<Point> などの特殊化された型については、将来的に自然な表記法を予約したいと考えています。 これを行うには、型消去されたジェネリクスでは、参照型のみが型引数として有効であることを要求します。 今日、人々は List<Integer> と書いています。 これは List<int.ref> および List<Point.ref> と一般化されます。

まとめ

私たちが取ったアプローチは、プリミティブと参照における既存の分断構造を維持する一方で、より規則的でパフォーマンスの高いものにすることです。 ボクシングはずっとコストが低くなって影の中に遠ざかり、「インライン」は「プリミティブ」の新しい言葉であり、プリミティブの集合は拡張可能です。 私たちは「重いボクシングを伴う参照とプリミティブ」から「軽いボクシングを伴う参照とインライン」に移行しましたが、2つの世界はほぼ同型のままです。

現在の世界 Valhalla
参照とプリミティブ 参照とインライン(プリミティブはインライン)
プリミティブにはボックスがあります インラインには参照射影があります
ボックスにはアイデンティティがあり、 getClass() で表示されます 参照射影は実行時に表示されません
ボクシングとアンボクシングの変換 インラインの縮小拡大変換、ただしルールは同じ
プリミティブは組み込みで、魔法 プリミティブはほとんど単なるインラインクラスで、追加のVMサポートを備えています
プリミティブにはメソッドもスーパータイプもありません プリミティブはインラインクラスであり、インラインクラスにはメソッドとスーパータイプがあります
プリミティブ配列は単相です インライン配列は多相です
@matarillo

This comment has been minimized.

Copy link
Owner Author

matarillo commented Jan 16, 2020

LW2 https://wiki.openjdk.java.net/display/valhalla/LW2 では参照射影 T.ref のことは 間接射影(indirect projection) T? という書き方がされてるっぽい

@matarillo

This comment has been minimized.

Copy link
Owner Author

matarillo commented Jan 16, 2020

@matarillo

This comment has been minimized.

Copy link
Owner Author

matarillo commented Jan 24, 2020

Logicoさんのブログ記事 https://logico-jp.io/2020/01/13/state-of-valhalla-section-2-language-model/ には「Kentaro Inomata さん (@matarillo) に翻訳いただき」と書いていただいていますが、私の翻訳はほとんどなくって、基本的にはLogicoさんの翻訳です。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.