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関連機能として、
は本JEPの対象外です。
Motivation
Javaのenumは便利な部品です。定数をグループ化し、それぞれの定数はシングルトンのオブジェクトになります。定数はボディを宣言することも可能で、enum宣言のベースの振る舞いをオーバーライドできます。以下の例ではenumでJavaのプリミティブ型のモデル化を試みていきます。まずは下記からスタートします。
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; } }
上記は一応問題ないですが制限があります。フィールドのboxClass
はClass<?>
という弱い型付けになっており、これはフィールドの型がenum定数で使用するすべての特定型(sharper types)*1で互換性が必要なためです。その結果、以下のようなコードを試みると、
Class<Short> cs = SHORT.boxedClass(); //error
コンパイルエラーとなります。さらに、defaultValue
フィールドはObject
型です。異なるプリミティブ型を表現する定数を単一のフィールドで共有する必要があるのでこれは避けられません。そのため、静的な安全性は無くなり、コンパイラは以下のようなコードを許容します。
String s = (String)INT.defaultValue(); //ok
上述の問題点は、enumとクラス間に固有の非対象性を取り除くことと、enum定数の型チェックの改良によって、対処が可能です。より正確に言うと、
これらの拡張により、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定数FLOAT
にmod
というメンバが無いのでコンパイラは上記の二つ目についてはエラーに出来ます。これにより型安全性を保証します。
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.ONE
とFoo.TWO
は同じ型Foo
となります。このルールは少なくとも以下2つの理由により望ましくありません。
- ジェネリックenumの場合、定数のstatic型はその定数の完全な型を捕捉するには不十分です。
- ジェネリックenumが無い場合であっても、enum定数にだけ定義されているメンバへクライアントからアクセスするのに定数型は不十分です(本JEPの冒頭の例を参照)。
この制約を越えるため、enum定数がそれ自身の型を得るようにenum定数の型付けを再定義すべきです。いまE
をenum宣言し、E
にC
(ジェネリック可)をenum定数宣言するとします。もし以下条件のどちらかが満たされる場合、定数C
は特定型に関連付けられます。
C
がC<T1, T2 ... Tn>
でボディ宣言無し。定数の特定型はE<T1, T2 ... Tn>
となる。C
がボディを持つ。定数の特定型は以下どちらかがスーパータイプとなる無名型(E.C
)になる。
これらの拡張される型付けによって、Foo.ONE
とFoo.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$1
に変更されます。この変更はTest
を使用する再コンパイルしていないクライアントコードを壊すことになります。
この問題を解決するには、イレイジャベースの方法がベターです。A
のstatic型を特定型Test.A
にする場合、定数型への参照はベースのenum型Test
をイレーズしたものとなります。これにより先述のものに反しないバイナリ互換性のコードとなります。しかし、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$1
とTest$2
は本質的には順序依存です。つまり、enum定数宣言の順序変更がバイナリ互換問題を発生させる可能性があります。とりわけ、上述のA
とB
を入れ替えてenumを再コンパイルすると、Test$1
はa
というメンバのメソッドを持たなくなっているため、クライアントのバイトコードはリンクに失敗します。これらはenumのバイナリ互換性のある拡張に関するJLSの言及内容と明らかに反しています。
enumの追加もしくは順序変更(reordering)が既存バイナリとの互換性を壊さないこと。
バイナリ互換を保つための別の方法に順序非依存のクラス名出力があります。例えばTest$1
とTest$2
の代わりにTest$A
とTest$B
となります。この変更のリフレクションとシリアライズに対する影響は以下に述べます。
Serialization
JavaのEnumはSerializableを実装しているのですべてのenumは暗黙的にserializableになります。本JEPの変更がシリアライズに関する互換性を保とうと我々は考えています。シリアライズ形式を変更すべきではありません。シリアライズ仕様 http://docs.oracle.com/javase/6/docs/platform/serialization/spec/serial-arch.html#6469 はenumに関する特別な扱いの記述があります。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 } }
このコードを実行すると、VMはIllegalAccessError
を出します。この問題は、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
- 引数で呼び出し = 1:69
つまり、上述のソース互換性問題は重大な問題を引き起こさないと考えられます。
Dependencies
enum定数で使われる特定型は必ずしもdenotableにならず、non-denotable型の別のカテゴリになります。このことはJEP-286 (Local Variable Type Inference)のnon-denotable型の扱いと相互に影響を及ぼす可能性があります。non-denotable型に関するJEP-286の決定に依存しますが、
var a = Argument.String;
Argument
ではなく特定型Argument.String
の型を持てるように出来るかもしれません。