State of Valhalla – Section 2: Language Model

このエントリは、Brian Goetzがアップデートした内容を反映したものです。アップデート前の内容は以下からご覧頂けます。

State of Valhalla – Section 2: Language Model (as of December 2019)
https://logico-jp.io/2020/01/13/state-of-valhalla-section-2-language-model-as-of-december-2019/

原文はこちら。
The original was written by Brian Goetz (Java Architect, Oracle).
http://cr.openjdk.java.net/~briangoetz/valhalla/sov/02-object-model.html

このドキュメントでは、インライン型を組み込むための言語モデルについて説明します。別のドキュメントで、インライン型のJVMモデル、およびJavaソースコードからJavaクラスファイルへの変換戦略について説明します(このドキュメントでは、インライン型を使わない現在の言語を“currently”(現在)と表現することにします)。

今の我々の位置

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

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

Types, current world
Types, current world

値集合

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

オブジェクトはクラスのインスタンスです。現在、すべてのオブジェクトには一意のオブジェクトアイデンティティがあります。参照型の値集合はオブジェクトではなく、オブジェクトへの参照で構成されます。String型の変数が取り得る値は、Stringオブジェクト自体ではなく、Stringオブジェクトへの参照です。

経験豊富なJava開発者にとってさえも、オブジェクトを直接保存、操作、またはアクセスできないことは驚きかもしれません。オブジェクト参照を扱うことに慣れているので、違いに気付かないことさえあります。実際、これはJavaオブジェクトが値渡しか参照渡しかという一般的な「落とし穴」の質問であり、答えは「どちらでもない」です。オブジェクト参照は値で渡されます。

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

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

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

Values, current world
Values, current world

現時点では、値はプリミティブ値とオブジェクトへの参照で構成されています。

現在の状況をまとめると以下の通りです。

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

インラインクラス

お膳立てが出来たので、ようやくJava言語の型システムでインラインクラスへの対処方法に取り組むことができます。インラインクラスのモットーは次のとおりです。

  • クラスのようなコード
  • intのように機能

このモットーの後半部分は、インライン型がこれまでに概説したプリミティブ型のランタイムの動作と整合する必要があることを意味します(実際、インライン型の傘下にプリミティブ型を含めたいと考えています。すでに2つに分かれている型システムを細分化するのは望ましくないからです)。

インラインクラスはクラスのようにコーディングされるため、クラスが持ち得るほとんどのもの(フィールド、メソッド、コンストラクタ、スーパーインターフェース、型変数など)を持つことが出来ます。

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
  • そのフィールドも暗黙的にfinal
  • サブクラスに対する制約がある(インターフェースといくつかの抽象クラスを拡張できる)

です。さらに、インラインクラスVの表現には、直接的であれ間接的であれ、Vという型のフィールドを含めることはできません(さらに、インラインクラスでは clone() や finalize() をオーバーライドできません)。

Valhallaでは型をプリミティブ型と参照型に分割するのではなく、インライン型と参照型に分割します。インライン型はプリミティブ型を包含します。「参照型」の意味はこれまでと同じで、インライン型ではないものです(これには宣言されたアイデンティティクラス、宣言されたインターフェイス、配列型などが含まれます)。型のダイアグラムを更新して、インライン型を含めたのが下図です。

Types, Valhalla
Types, Valhalla

値集合

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

Values, Valhalla
Values, Valhalla

プリミティブ型のように、インラインクラスの値集合は当該クラスのインスタンスの集合であり、オブジェクト参照ではありません。

すべての型にはデフォルト値があります。

説明備考
プリミティブ型ある種のゼロ0、0.0、falseなど
参照型null
インライン型フィールドの各々の型のデフォルト値

Cというクラス型の場合、Cのデフォルト値をC.defaultと記載します(ジェネリックなコードでは、T.defaultと表現する場合があります。型消去されたジェネリクスの場合、Tは常に参照型であるため、これはnullと評価されます。ジェネリクスが特殊化されると、T.defaultも特殊化されます)。

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

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

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

さらに、変数型(およびジェネリックな型の境界)でのアイデンティティの要件を静的に反映します。

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

等値性

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

コンポジション (composition) を使い、==をインラインオブジェクトに拡張できます。具体的には、2つのインラインオブジェクトが同じ型で、フィールドの各々が当該フィールドの静的な型の==に従って両者が等しい(Float::equalsおよびDouble::equalsのセマンティクスに従って比較するfloatとdoubleを除く)場合に、両インラインオブジェクトは==です。この定義では、2つのインラインオブジェクトが置換可能である、つまり違いを識別できない場合にのみ等値であると言います。

配列

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

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

アイデンティティに依存する操作

現在、オブジェクトアイデンティティの観点から定義されている操作があり、これらの操作の中には、すべてのオブジェクトインスタンスをカバーするように適切に拡張できるものもあれば、部分的な拡張になるものもあります。以下はその例です。

操作説明
Equality (等値性)Object上で==を全域化する。
現在において意味がある場合に、この新しい定義はその意味と一致する(次章で説明するように、インラインオブジェクトへの参照に関して、ここで追加の作業を行う必要がある)。
System::identityHashCodeidentityHashCodeの主な用途は、IdentityHashMapなどのデータ構造を実装すること
等値性の場合と同様、identityHashCodeを全域化できる(すべてのフィールドのハッシュからインラインオブジェクトのハッシュを導出できる)。
Synchronization (同期)部分的な操作
実行時に同期が失敗することを静的に検出できる場合(インラインクラスでの同期メソッドの宣言を含む)、コンパイルエラーを発行できる。そうでない場合、インラインインスタンスをロックしようとすると、実行時にIllegalMonitorStateExceptionが発生する。
これを正当化できるのは、当該オブジェクトのロックプロトコルを明確に理解していない状態でオブジェクトをロックする(例えば、任意のオブジェクトまたはインターフェイスインスタンスのロック)ことは本質的に軽率なことだからである。
Object::wait
Object::notify
同期の場合と同様
Weak references (弱参照)どちらの方法も考えられる。
Object上で弱参照作成の部分化もできるが、厄介な結果をもたらす。例えば、何らかの弱いデータ構造を維持したいすべてのクラスは、アイデンティティオブジェクトとインラインオブジェクトの別々のパスに分岐する必要があるため、弱参照はほとんど役に立たなくなる(これはidentityHashCodeの部分化に類似)。
一方、単純な動作(例えば、インラインオブジェクトが保持されている弱参照を決して消去しない)を選択すると、そもそも弱参照を使っているGCに適した振る舞いの一部が失われてしまう(この点ではさらなる分析が必要)。

インライン型と参照型

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

現時点では、ボクシング変換によってプリミティブ型から参照型に変換しています。この変換は、サブタイピングまたはボクシングのいずれかによって、任意の値をObjectで表すことができるため、より多様なコードを記述できて便利ですが、ボクシングには多くの深刻な欠点があります。ボクシングした結果の型はカスタマイズされた手作りのクラスであり、プリミティブ型との言語的なつながりは限られているため、インラインクラスに確実に対応しません。さらに悪いことに、ボクシング後に生成されたオブジェクトには、多くのVM最適化を妨げる「偶発的な」オブジェクトアイデンティティがあります。「ボクシングは遅い」という信念は、この偶発的なアイデンティティに由来するものです。

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

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

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

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

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

Valhallaのvalue(値)の世界はプリミティブ型の値、インラインオブジェクト、アイデンティティオブジェクトとインラインオブジェクトの両方への参照で構成されます。

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

インライン拡大変換を使うと、ボクシングによるパフォーマンス劣化がほとんどなく、所望のボクシングセマンティクスを得ることができます。

演算子ref vは、vがインラインオブジェクトの場合、vへの参照、vがオブジェクト参照である場合はv自体、として定義すると便利です。refは、すべての表現可能な値を総合したものであり、常に参照を返します(反対の意味である演算子unrefは部分的であり、インラインオブジェクトへの参照にのみ適用され、両者は射影と埋め込みのペアを形成します)。

インライン拡大変換はインライン型から、インライン型が実装する任意のインターフェースやオブジェクトにまで存在し、ref演算子のアプリケーションとして定義されています。これにより、Iの値集合に関する疑問に回答できます。つまり、Cのすべてのインスタンスへの参照と、Vのすべてのインスタンスへの参照が含まれます。

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

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

この時点で、読者は、これまで使われてきたボクシング (boxing) という用語を、まだ使われていない用語のインライン拡大 (inline widening) に置き換え、単に命名法でだまそうとしただけでは、と思われるかもしれません。やったことが名称変更だけなのであれば、確かにだましているだけでしょう。インライン拡大がボクシングの単なる名称変更でない理由は、Valhalla JVMでは、インライン拡大および縮小変換が、対応するボクシングおよび案ボクシング変換よりもはるかに軽量だからです。ボクシングの問題は、アドホックであり、コストが大きいことです。これらの両方の懸念に対処しようとしています。

Supertypes

インラインクラスはインターフェースを実装できます。任意のクラスを拡張することはできませんが、一部の制限された抽象クラスを拡張できます。その条件は以下の通りです。

  • クラスにフィールドがない
  • 空の引数を取らないコンストラクタだけが存在する
  • インスタンスイニシャライザがない
  • synchronizedメソッドがない
  • 当該クラスのスーパークラスが全てこの条件を満たす(Objectはそのようなクラスの一例です)

参照射影と値射影 (Reference and value projections)

特定のインラインクラスのオブジェクトへの参照の集合に加えてnullを表現できると便利な場合があります。インライン型Vを指定すると、値セットが次のように指定される参照型Rが必要です。

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

このような型RをVの参照射影 (reference projection) と呼びます。参照型はそれ自身の参照射影です。

参照射影は、現在のラッパークラスが果たす役割を果たしますが、すべてのインラインクラスに手書きのアドホックなラッパーを持たせたくありません。インラインクラスから参照射影を機械的に導出し、それを参照するための統一された方法が必要です。この方法では、インラインクラスを参照射影にマッピングする心的語彙 (mental dictionary) を維持する必要はありません。任意の型Tについて、T.refがTの参照射影を示すようにします。

インラインクラスの場合、参照射影と値射影の両方を自動的に作成します。インラインクラスVの場合、参照射影、値射影は以下のように表記します(参照射影は、サブタイプとしてV.valのみを許可する、シールされた抽象クラスです)。

  • V.ref : Vへの参照射影(Vのインスタンスへの参照集合に加えてnullも含まれる)
  • V.val : Vへの値射影(Vのインスタンスの集合)
  • V : (以下に示す特別なことがない場合)V.valのエイリアス

インラインクラスVの場合、以下のように表せます。

sealed abstract class V.ref permits V.val { }
inline class V.val extends V.ref { }

そしてVをV.valのエイリアスにします。V.valからV.refへ(およびVからV.refへ)のインライン拡大変換を自動的に取得します。さらに、V.refからV.valへ(およびV.refからVへ)のインライン縮小変換を定義します。これは、unref演算子を適用し、null時にはNullPointerExceptionをスローします。

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

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

クラスミラー (Class mirrors)

インラインクラスVは、2つの型(V.refとV.val、および型エイリアスV)を生成するのと同時に、2つのクラスミラーも生成します。ただし、参照射影は抽象クラスであるため、参照射影のインスタンスであることを報告するインスタンスはありません。Vという値への非NULL参照は、V.valのインスタンスであると引き続き報告します。(参照射影のクラスミラーは、プリミティブ型の合成クラスミラーと同じ目的を果たします。これにより、フィールド型もしくはメソッドパラメーターとして使用する場合、リフレクションは値射影と参照射影を区別できます)。

インターフェース

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

  • Conformance
    クラスは、メンバーとして、インターフェースのすべてのメンバーを持つ
  • Transitivity
    このクラスの任意のサブクラスもまたインターフェースを実装する
  • Subtyping
    クラス型はインターフェース型のサブタイプ

この最後の箇条書きのサブタイピングを、インラインクラスをサポートするよう少しずつ修正する必要があります。クラス型の参照射影は、インターフェイス型のサブタイプであると言います(アイデンティティクラスの型は、それ自体の参照射影ゆえ、このステートメントはすべてのクラスに適用されます)。同様に、インラインクラスが抽象クラスを拡張する場合、これは参照射影が抽象クラスのサブタイプであることを意味します。

オブジェクト

全てのクラス、インラインとアイデンティティのルート型としての役割ゆえに、Objectは多くの特性をインターフェースと共有します。既述の通り、全てのインライン型からObjectへのインライン拡大変換があります。

しかしながら、Objectは具象クラスなので、残念ながらObjectを直接コンストラクタを使ってインスタンス化できてしまいます。そして、インターフェースを継承するため、ObjectはInlineObjectもIdentityObjectも実装できません。しかし、new Object() でインスタンスを生成した場合、アイデンティティ型のインスタンスでなければなりません(他の理由でObjectのインスタンス化するポイントがないためです)。

このtrapからうまく切り抜けるには手の込んだ動きが必要です。IdentityObjectを返すstatic factoryメソッドであるObject::newIdentityを作成し、つづいてさまざまなツール(コンパイラーの警告、JITマジック)を使用して、new Object() の既存のソースおよびバイナリでの利用をこの方向に移行していき、最終的には(protectedにすることで)直接インスタンス化するためのObjectコンストラクタを非推奨にします。

再び等値性

インラインオブジェクトに対する== の拡張はまだ終わっていません。インラインオブジェクト自体に対しては定義したものの、参照型の値集合でインラインオブジェクトへの参照を持つ可能性がある参照型に対してはまだなので、以下の条件を満たす場合に等値であるとします。

  • 2個のオブジェクト参照がともにnullである
  • (または)両者とも同じアイデンティティオブジェクトへの参照である
  • (または)両者が等値のインラインオブジェクトへの参照である

これにより==の代入可能性のセマンティクスを全ての値に拡張します。つまり、NaNの従来の動作を除き、どのような方法でも両者を区別できない場合にのみ2値は==です。

これにより、==に関する以下の有用な不変式を得ることができます(NaNの従来の動作を法とするすべて)。

  • ==は再帰的である。
    すべてのvについて、v == v
  • 2つのインライン値を参照に拡大すると、初期値がそうであった場合にのみ、結果は==になる。
  • 2つの参照をインラインオブジェクトに縮小すると、元の参照があった場合にのみ、結果は==になる。

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

参照射影が必要な理由

参照射影の定義は理に適っており、参照型との関係についてのこれまでの直観と一致していますが、これらの型が非常に重要な理由をまだ完全に説明してきてはいません。

以下のような場合にはインライン型を利用できません。

理由説明
NullityNullはインライン型の値ではないが、全ての参照型の値である。
しかし、時として「型VのNullを許容する値」という概念を表現したいことがある。
Non-flatteningインライン型の値は定期的にオブジェクトと配列に平坦化され、通常これで問題ない。ただ場合によっては、例えば、「幅の広い」インラインクラス(多くのフィールドを持つクラス)があり、それらの疎らな配列が必要な場合に、メモリ使用率をより細かく制御したい場合がある。平坦化された値の配列ではなく、参照の配列を使用すると、メモリ効率が向上する場合がある。
Recursive representationインラインクラスは、再帰的に自分自身を表現できない。
通常、これは重大な制限ではないが、(「next」フィールドを持つNodeクラスなど)参照型でこれをシミュレートしたくなることがある。
Erased generics既存の型消去ジェネリクスの場合、型パラメーターが参照型であることを前提としている。
アイデンティティに依存する操作やnull値などを許容する。特殊化されたジェネリクスができるまで、参照型を型パラメーターとして使用したい。
No sensible zeroインライン型のデフォルト値は、すべてのフィールドがデフォルト値をとる値。
一部の型では、この値で問題ない( (0,0) はPointにとって問題ないデフォルト値)が、場合によっては(Rationalなど)、この値は無意味であるか、さらには危険な場合がある。この場合、nullを使用して初期化されていない値を表すことができるよう、参照型を使いたいことがある。

これらの状況すべてにおいて、インライン型ではなく参照型を使用できます。しかし、Objectなどの幅広い参照型を使用すると、多くの損失が発生します。型の安全性が失われ、クライアントはコードに未チェックのキャストを挿入してインラインドメインに戻る必要があります。インライン拡大・縮小変換を伴うインライン型の参照射影を使用することで、特定のインラインクラスの値集合と密に結合した値集合を持つ参照型を持ち、この参照型を明示的な変換を使わずにインラインクラスに戻すことが自由にできます。これにより、型安全と所望する利便性を取り戻します。

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

public V.ref get(K key);

この表現で、Map::getの戻り値の型がVへの参照もしくはnull参照のいずれかになるという概念を記録しています。

移行

これまでに議論したテクニックは新規のコードにとってはよいのですが、移行する場合には、準備しておきたいシナリオがいくつかあります。

値ベースのクラスからインラインクラスへの移行

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

Value-based Classes
https://docs.oracle.com/javase/8/docs/api/java/lang/doc-files/ValueBased.html

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

次善の策は、Optionalをインラインクラス(privateクラスなど)の参照射影として定義することです。つまり、Optionalを抽象クラスに移行し、インラインクラスを1つだけ許可するようにsealするように手配します(値射影Optional.val

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

このトリックを達成するために、参照射影Optional.refと値射影Optional.valを生成するOptionalのインラインクラスを宣言したいのですが、エイリアスOptionalを逆にして、値射影ではなく参照射影を参照するようにしたいのです。これは、宣言を変更してref-defaultインラインクラスであると宣言することで実現できます。

ref-default inline class Optional<T> {
    // current implementation of Optional
}

ref-default修飾子が持つ唯一の効果は、装飾されていない型名が2つの射影のどちらを参照するかを決定することです。

このような移行から生じる可能性のある非互換モードが1つあります。それはクライアントがgetClass()の結果をOptional.classと比較する場合です。移行前の段階では、OptionalのインスタンスにはクラスOptional.classがありますが、Optionalを抽象クラスに移行すると、Optionalのインスタンスは、これらが値射影Optional.valのインスタンスであると報告します。

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

移行: プリミティブ

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

Optionalを移行するのと同じテクニックを使って、ラッパー型Integerとその仲間をシールされた抽象クラスに移行することからはじめます。これらのインターフェースはプリミティブの参照射影になるでしょうが、このためには、まずユーザーをプリミティブラッパーのアイデンティティへの依存から引き離す必要があります。publicなコンストラクタを削除するために非推奨にして (実際に削除するのではなく、privateにする)、一定の移行期間はIntegerインスタンスを「永続的にロック」します(このアプローチはJEP 169を参照してください)。

JEP 169: Value Objects
http://openjdk.java.net/jeps/169

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

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

inline class int { /* implementation */ }

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

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

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

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

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

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

まとめ

これまでのアプローチは既存のプリミティブー参照の分断の構造を維持しつつ、より規則的でパフォーマンスに優れたものにすることです。ボクシングはより安価になって影の奥に遠ざかり、「インライン」が「プリミティブ」の新しい言葉であり、プリミティブの集合は拡張可能です。「重いボクシングを使う参照とプリミティブ」から「軽いボクシングを使う参照とインライン」に移行してきましたが、両世界はほぼ同一構造のままです。

Current WorldValhalla
参照とプリミティブ参照とインライン
(プリミティブはインライン)
プリミティブにはボクシング変換があるインラインには参照射影がある
ボクシングの結果アイデンティティを持つ
getClass()で見える
参照射影は実行時には見えない
ボクシング/アンボクシング変換インライン縮小・拡大変換
(ただしルールは同じ)
プリミティブは組み込みで魔法プリミティブはほぼインラインクラス
追加のVMサポートを備える
プリミティブにはメソッドやスーパータイプはないプリミティブはインラインクラス
メソッドやスーパータイプ(スーパークラス、スーパーインターフェース)を持つ
プリミティブの配列はmonomorphic(単型性)インラインの配列はpolymorphic(多型性)

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中