kagamihogeの日記

kagamihogeの日記です。

JEP 301: Enhanced Enumsをテキトーに訳した

http://openjdk.java.net/jeps/301 をテキトーに訳した。

JEP 301: Enhanced Enums

Owner    Maurizio Cimadamore
Created 2016/11/25 11:27
Updated 2016/12/08 18:51
Type    Feature
Status  Candidate
Component   tools?/?javac
Scope   SE
Discussion  platform dash jep dash discuss at openjdk dot java dot net
Effort  M
Duration    M
Priority    3
Issue   8170351
Relates to  JEP 286: Local-Variable Type Inference

Summary

Java言語のenum定数の表現力を、enum型変数(generic enums)により機能強化することで、enum定数の型チェックをより詳細なものにします。

Goals

二つの機能拡張を共に使うことで、enum定数が定数固有の型情報と定数固有の状態と振る舞いを持てるようにします。開発者が望ましい結果を実現するためにenumをクラスに置き換えなければならないケースが多数存在します。機能拡張によりそのようなケースは減少すると思われます。

以下の例は二つの機能拡張を共に使用しているものです。

enum Argument<X> { // declares generic enum
   STRING<String>(String.class), 
   INTEGER<Integer>(Integer.class), ... ;

   Class<X> clazz;

   Argument(Class<X> clazz) { this.clazz = clazz; }

   Class<X> getClazz() { return clazz; }
}

Class<String> cs = Argument.STRING.getClazz(); //uses sharper typing of enum constant

Non-Goals

本JEPではenum定数の型チェックの機能拡張は目的としません。例えば、別のenum関連機能として、

  • enumのサブクラス可能化
  • 非staticコンテキストでのenum使用可能化

は本JEPの対象外です。

Motivation

Javaenumは便利な部品です。定数をグループ化し、それぞれの定数はシングルトンのオブジェクトになります。定数はボディを宣言することも可能で、enum宣言のベースの振る舞いをオーバーライドできます。以下の例ではenumJavaのプリミティブ型のモデル化を試みていきます。まずは下記からスタートします。

enum Primitive {
    BYTE,
    SHORT,
    INT,
    FLOAT,
    LONG,
    DOUBLE,
    CHAR,
    BOOLEAN;
}

上述のように、enum宣言はクラスのように行い、コンストラクタを持てます。コンストラクタを用いてボックスクラスの保持と各プリミティブのデフォルト値を指定します。

enum Primitive {
    BYTE(Byte.class, 0),
    SHORT(Short.class, 0),
    INT(Integer.class, 0),
    FLOAT(Float.class, 0f),
    LONG(Long.class, 0L),
    DOUBLE(Double.class, 0d),
    CHAR(Character.class, 0),
    BOOLEAN(Boolean.class, false);

    final Class<?> boxClass;
    final Object defaultValue;

    Primitive(Class<?> boxClass, Object defaultValue) {
       this.boxClass = boxClass;
       this.defaultValue = defaultValue;
    }

}

上記は一応問題ないですが制限があります。フィールドのboxClassClass<?>という弱い型付けになっており、これはフィールドの型がenum定数で使用するすべての特定型(sharper types)*1で互換性が必要なためです。その結果、以下のようなコードを試みると、

Class<Short> cs = SHORT.boxedClass(); //error

コンパイルエラーとなります。さらに、defaultValueフィールドはObject型です。異なるプリミティブ型を表現する定数を単一のフィールドで共有する必要があるのでこれは避けられません。そのため、静的な安全性は無くなり、コンパイラは以下のようなコードを許容します。

String s = (String)INT.defaultValue(); //ok

上述の問題点は、enumとクラス間に固有の非対象性を取り除くことと、enum定数の型チェックの改良によって、対処が可能です。より正確に言うと、

  • enum宣言に型引数を使えるようにする。
  • enum定数に関連付けられた特定型情報の事前削除(prematurely erase)を行わない。

これらの拡張により、Primitive enumは以下のように書き換えられます。

enum Primitive<X> {
    INT(Integer.class, 0) {
       int mod(int x, int y) { return x % y; }
       int add(int x, int y) { return x + y; }
    },
    FLOAT(Float.class, 0f)  {
       long add(long x, long y) { return x + y; }
    }, ... ;

    final Class<X> boxClass;
    final X defaultValue;

    Primitive(Class<X> boxClass, X defaultValue) {
       this.boxClass = boxClass;
       this.defaultValue = defaultValue;
    }
}

以前のものと比べるとジェネリック宣言によって明確さが向上しています。enum定数Primitive.INTは特定パラメータ型Primitiveを持ち、そのメンバは特定型になります。

Class<Short> cs = SHORT.boxedClass(); //ok!

また、enum定数の型情報は事前削除されないため、コンパイラは定数のメンバについて推論可能になります。例は以下になります。

int zero_int = INT.mod(50, 2); //ok
int zero_float = FLOAT.mod(50, 2); //error

enum定数FLOATmodというメンバが無いのでコンパイラは上記の二つ目についてはエラーに出来ます。これにより型安全性を保証します。

Description

Generic enums

JDK-6408723で議論されたように、enumジェネリクスを適用可能にする重要な要求があり、その内容は型パラメータをenum定数宣言に完全にバインドします。これにより現行のenumを補強するシンプルな変換スキーマ(straightforward translation scheme)が使用可能になります。例えば、以下のようなenum宣言があるとき、

enum Foo<X> {
   ONE<String>,
   TWO<Integer>;
}

糖衣構文を除去した後の同等なコードは以下になる予定です。

/* enum */ class Foo<X> {
   static Foo<String> ONE = ...
   static Foo<Integer> TWO = ...

   ...
}

型のバインディングは静的に与えられているため、各定数とstaticフィールド宣言とのマッピングは可能です。

enum定数の初期化にはダイアモンドを使えることが望ましいです。たとえば、

enum Bar<X> {
   ONE<>(Integer.class),
   TWO<>(String.class);

   Bar(X x) { ... }
}

ダイアモンドを使う場合、enum定数にボディ(=無名クラスに変換される)があり、かつ、推論型がnon-denotableの場合、特別な配慮が必要となります。無名内部クラスにダイアモンドがある場合、コンパイラはそのダイアモンドを拒否する必要があります。

Sharper typing of enum constants

現行のルールでは、enum定数のstatic型はenum自身の型となります。このルールでは、上述の定数Foo.ONEFoo.TWOは同じ型Fooとなります。このルールは少なくとも以下2つの理由により望ましくありません。

  • ジェネリックenumの場合、定数のstatic型はその定数の完全な型を捕捉するには不十分です。
  • ジェネリックenumが無い場合であっても、enum定数にだけ定義されているメンバへクライアントからアクセスするのに定数型は不十分です(本JEPの冒頭の例を参照)。

この制約を越えるため、enum定数がそれ自身の型を得るようにenum定数の型付けを再定義すべきです。いまEenum宣言し、ECジェネリック可)をenum定数宣言するとします。もし以下条件のどちらかが満たされる場合、定数Cは特定型に関連付けられます。

  • CC<T1, T2 ... Tn>でボディ宣言無し。定数の特定型はE<T1, T2 ... Tn>となる。
  • Cがボディを持つ。定数の特定型は以下どちらかがスーパータイプとなる無名型(E.C)になる。

これらの拡張される型付けによって、Foo.ONEFoo.TWOのstatic型はそれぞれ異なるものに出来ます。

Additional Considerations

Binary compatibility

以下のenumがあるとします。

enum Test {
   A { void a() { } }
   B { void b() { } }
}

前述の通り、以下のように変換されます。

/* enum */ class Test {
   static Test A = new Test() { void a() { } }
   static Test B = new Test() { void b() { } }
}

enum定数に特定型を持たせるには、素朴なやり方として以下のような変換が考えれます。

/* enum */ class Test {
   static Test$1 A = new Test() { void a() { } }
   static Test$2 B = new Test() { void b() { } }
}

これは明らかにバイナリ非互換です。再コンパイル時にenum定数Aの型はTestからTest$1に変更されます。この変更はTestを使用する再コンパイルしていないクライアントコードを壊すことになります。

この問題を解決するには、イレイジャベースの方法がベターです。Aのstatic型を特定型Test.Aにする場合、定数型への参照はベースのenumTestをイレーズしたものとなります。これにより先述のものに反しないバイナリ互換性のコードとなります。しかし、Testへの参照がすべてイレーズされる場合、特定のenum定数のメンバへのアクセスの実装方法はどうすれば良いでしょうか?

Foo.A.a();

上記コードの場合、Aへのシンボリック参照はTestにイレーズされ、メソッド呼び出しは型付け(well-typed)されません(Testにはaという名前のメンバは無いため)。この問題を解消するには、コンパイラはsynthetic cast*2を挿入する必要があります。

checkcast Test$1
invokevirtual Test$1::a

このsynthetic castは、イレーズを経由する交差型(intersection type through erasure)のメンバアクセスと相似形になっていません。

別の直交する所見(orthogonal observation)として、現行のenum定数クラスのネーミング規則は壊れやすい、があります。上述のTest$1Test$2は本質的には順序依存です。つまり、enum定数宣言の順序変更がバイナリ互換問題を発生させる可能性があります。とりわけ、上述のABを入れ替えてenumを再コンパイルすると、Test$1aというメンバのメソッドを持たなくなっているため、クライアントのバイトコードはリンクに失敗します。これらはenumバイナリ互換性のある拡張に関するJLSの言及内容と明らかに反しています。

enumの追加もしくは順序変更(reordering)が既存バイナリとの互換性を壊さないこと。

バイナリ互換を保つための別の方法に順序非依存のクラス名出力があります。例えばTest$1Test$2の代わりにTest$ATest$Bとなります。この変更のリフレクションとシリアライズに対する影響は以下に述べます。

Serialization

JavaEnumはSerializableを実装しているのですべてのenumは暗黙的にserializableになります。本JEPの変更がシリアライズに関する互換性を保とうと我々は考えています。シリアライズ形式を変更すべきではありません。シリアライズ仕様 http://docs.oracle.com/javase/6/docs/platform/serialization/spec/serial-arch.html#6469enumに関する特別な扱いの記述があります。enum定数のシリアライズ形式はその名前についてだけで、また、enum定数のserialization/deserializationのカスタムは不可能です。(すべてのenum定数は<clinit>中に初期化され、デシリアライズenumのstatic values()を呼び出すとEnum.valueOfメソッドが使われます。この動作によりベースenumクラス(とすべての定数)の初期化を暗黙的に強制します。)

つまり、シリアライズ形式はコンパイラが生成するクラス名に依存していないため、既存のシリアライズ形式に関する互換性問題はありません。

Reflection

バイナリ名が出てくる別の領域としてリフレクションがあります。以下は完全に問題の無いリフレクションのコードです。

Class<?> c = Class.forName("Test$1");
System.err.println(c.getName()); //prints Test$1

リフレクションにはenum定数をインスタンス化させないための制約がありますが、enum定数クラスのメンバのインスペクションには制約がありません。上記のイディオムを使用する既存コードは本JEPでenum定数のバイナリ形式を変更すると動作しなくなります。

Denotability

現状、enum定数は値であり型ではありません。よって、enum定数もdenotable typesにすべきかどうか、は妥当な質問です。

The usual arguments apply here - しかし、enum定数にdenotable typeを持たせることでマジックを減らし*3プログラマenum定数の型で変数宣言が可能となります。しかし、欠点もあります。

  • 同一の識別子が値と型の両方を意味するため、コードの可読性を下げる可能性がある(例: A a = A)。
  • すべてのenum定数がそれ自身の型を持つかどうかは事前には明確ではない。メンバを宣言しないenum定数の場合は? 型がベースのenum型のエイリアスに過ぎない場合は?

enum定数型がnon-denotable typeの場合、プログラマは間接的な相互作用(例:型推論経由)のみ可能となる不透明さが発生します。non-denotable typeの欠点を和らげる、ローカル変数型推論の追加提案に注目しておく必要があります。これは、enum特定型がnon-denotableであっても、その型で変数宣言を技術的には可能とします(例:var a = A)。

Accessibility

enum特定型でのメンバのアクセス性に関するコーナーケースがあります。以下の例を考えます。

package a;

public enum Foo {
  A() { 
    public String s = "Hello!";
  };
}

package b;

class Client {
   public static void main(String[] args) {
      String s = Foo.A.s; //IllegalAccessError
   }
}

このコードを実行すると、VMIllegalAccessErrorを出します。この問題は、enum定数Foo$Aの無名クラスがパッケージプライベートになるため、別パッケージからパッケージプライベートのクラスのpublicフィールドにアクセスしようとしてアクセスエラーになります。この問題を解決するには、enum定数クラスはenumクラスの定義と同じ修飾子になるべきです。

Source compatibility

ソース互換性の観点では、本JEPの機能と型推論との相互作用時に漏れが出る特定型のケースが存在します。以下のコードを考えます。

EnumSet<Test> e = EnumSet.of(Test.A);

上記コードは比較的よく使われるものです。Test.Aのstatic型は単にTestで、両方ともTestという名前の型の制約を持つため、EnumSet.ofの型変数の推論はシンプルです。しかし、Test.Aが型チェックされるような変更をすると、その振る舞いは微妙になり、EnumSet.ofの型変数は二つの競合する制約を取得します。Test(ターゲット型)と等しくかつTest.Aのスーパータイプでなければなりません。幸い、こうした場合には、型推論はより厳密な等価制約を選ぶようなスマートさを備えているため、結果として推論はTestになります。本JEPの変更のソース互換性への影響はJDK-8075793とは異なっており、where the change caused capture variables to appear in more places instead of their upper bounds.

Risks and Assumptions

上述の概要の通り大きく二種類のリスクが本JEPの提案には存在します。

  • enum定数のバイナリ名の変更がリフレクションの中核機能に問題を引き起こす可能性がある。
  • enum定数の型付けに関する変更が、とりわけターゲット型が無い場合のメソッドの型推論に微妙な変更を引き起こす可能性がある。

前者の問題はおそらくあまり心配する必要は無く、上述の通り、enum定数のバイナリ名は現状大変壊れやすくて再順序付けの問題を引き起こしがちです。そのため、enum定数のバイナリ名に依存するコードは本来的に壊れやすく、その理由は本質的に特定のコンパイラの出力に依存しているからです。

後者の問題は、潜在的にソース互換性の問題になりうるため、より厄介です。上述のソース非互換性問題がどの程度発生しうるかを特定するため、我々は様々な引数でEnumSet.ofがどの程度呼ばれているか計測しました。各呼び出しについて、ターゲット型が利用可能なコンテキストで呼び出せるかどうかを追跡しました。以下はその結果です(計測はすべてのオープンなJDKリポジトリ群に対して実施)。

  • EnumSet.ofの呼び出し: 150
    • 引数で呼び出し = 1:69
      • そのうちで、ターゲット型無し: 0

つまり、上述のソース互換性問題は重大な問題を引き起こさないと考えられます。

Dependencies

enum定数で使われる特定型は必ずしもdenotableにならず、non-denotable型の別のカテゴリになります。このことはJEP-286 (Local Variable Type Inference)のnon-denotable型の扱いと相互に影響を及ぼす可能性があります。non-denotable型に関するJEP-286の決定に依存しますが、

var a = Argument.String;

Argumentではなく特定型Argument.Stringの型を持てるように出来るかもしれません。

*1:文脈から明らかのとおりClass<?>ではないClass<Integer>とかのことを指してるわけだが、良い日本語浮かばなかったので、とりあえずこう訳した。

*2:java syntheticとかでぐぐれば出てくるが、コンパイラコンパイル時にメソッドやフィールドを合成(synthetic)することを指しているらしい。

*3:makes it less magicが原文。マジックナンバーとかそういうのかと。