捕捉変換に関する覚え書き

まじめにJava型推論書いてるのでメモ。capture-#xとか分かるようになるかも。
レビューに付き合ってくださる方は Java SE Specifications からPDFをダウンロードしておくとよいかと*1

捕捉変換の概要

捕捉変換(capture conversion)は言語仕様の「5.1.10 Capture Conversion」にあります。5回くらい読まないとまともに理解できないので趣旨だけ簡単に

  • パラメータ化型*2に出現する型引数にはワイルドカード*3を含めることができる
  • ワイルドカードは型そのものではなくて型の範囲(集合)を表現する*4
  • 変数の参照などの式には捕捉変換が適用され、パラメータ化型からワイルドカードが除去される
  • ワイルドカードを含むパラメータ化型に捕捉変換を適用すると、すべてのワイルドカードが一時的な型変数に置き換えられる
    • コンパイルエラーの時によく出る"capture-#xxx"は、捕捉変換で生成された一時的な型変数
  • ワイルドカードが一時的な型変数(Xとおく)として表現されていれば、メソッド呼び出し時の型推論でXを利用した推論ができる

最後のが非常にわかりにくいのですが、言語仕様にある例を使って簡単に。

public static void reverse(List<?> list) {
    rev(list); // listは捕捉変換でList<X>型になる
}
private static <T> void rev(List<T> list) {
    // ... listの内容を反転させるコード
}

上記のコードでは、rev(list) が呼び出される際に list に捕捉変換が適用され、listという式の型はList<?>ではなくList<X>型となります(Xは一時的な型変数)。List<?>型ではlistに含まれる要素は未知の型でしたが、?を捕捉してXとおくことで「未知ではあるけど少なくとも型変数Xで表せる型」ということになります。
そして、rev(list)でメソッド呼び出し時の型推論が行われますが、ここでTはXであるという推論が行われれば、無事にrevメソッドを呼び出すことができるようになります。

捕捉変換の適用箇所

この辺。

  • 6.5.6 Meaning of Expression Names
  • 15.11 Field Access Expressions
  • 15.12.2.6 Method Result and Throws Types
  • 15.13 Array Access Expressions
  • 15.16 Cast Expressions
  • 15.25 Conditional Operator ? :
  • 15.26 Assignment Operators

基本的には、

  • 変数を参照するとき
  • メソッドの戻り値
  • キャストの結果

と、「明示的に書いた型からワイルドカードを捕捉しなきゃいけない時」に主に利用されます。変数やメソッドは宣言時に型を記述しますし、キャストでは直接型を記述します。例外としてあるのは三項演算子を使った式条件演算式*5で、こいつは式の型を計算する際に最小の上限境界*6を計算しなきゃならず、その過程でワイルドカードを含む型が出現する場合があるためだと考えます。

重要な点は、変数を参照(read)する際にはその式に捕捉変換が適用されますが、書き込み先の変数が持つ型には捕捉変換が適用されません。もし書き込み先の変数に捕捉変換が適用されてしまうと、ワイルドカードが無力化されます。

void a(List<?> list1) {
    List<?> list2;
    list2 = list1; // ok.
}

上記はコンパイルできるJavaのコード片です。「list2 = list1」について考えた際、list1は変数の参照ですので捕捉変換が適用され、list1の式の型はList<X1>型(X*は一時的な型変数)となります。list2はList<?>型ですのでサブタイプ関係が成り立ちます*7
ここで、list2にも捕捉変換が適用されList<X2>になると仮定した場合、擬似的には下記のようになります。

<X1, X2> void a(List<X1> list1) {
    List<X2> list2;
    list2 = list1; // error
}

とまぁ、こんな感じで変数は参照の際にのみ捕捉変換が適用されます。メソッド呼び出し時の仮引数なんかも同様です。

捕捉変換によって生成される型変数の規則

最後に捕捉変換で作られる型変数(XとかX1とかおいてたアレ)について。言語仕様にはこの部分だけしっかり書いてあるのですが、ここまでの内容が腑に落ちるまでイマイチ理解できませんでしたとさ。

ここで考えるのは、G<A>という形式のパラメータ化型とします。なお、Aは型そのものではなくワイルドカードが入ることが。
さらに、Gの宣言はG<F extends U>、つまり先ほどのAの部分は上限境界にUを持つものとします。上限境界がない場合はUをjava.lang.Object型だと読み替えればOK。

Aの形式によってG<A>に捕捉変換を適用した結果が変わります。

  1. Aがワイルドカードでない場合
    • 捕捉変換の結果は G<A>
  2. Aが?(境界を持たないワイルドカード)である場合
    • 捕捉変換の結果は G<T>,
      • ただし T は上限境界 U'を持つ新たな型変数
      • U'は型Uに出現するFをTに置き換えた型, つまり、G<E extends Enum<E>>ではU=Enum<E>, F=Eなので、U'はEnum<T>となる
  3. Aが? extends B(上限境界Bを持つワイルドカード)である場合
    • 捕捉変換の結果は G<T>,
      • ただし T は上限境界 B & U' を持つ新たな型変数*8
      • U'の解釈は?のときとおなじ
  4. Aが? super B(下限境界Bを持つワイルドカード)である場合
    • 捕捉変換の結果は G<T>,
      • ただし T は上限境界 U', 下限境界Bを持つ新たな型変数
      • U'の解釈は?のときとおなじ

と言った感じです。なお、パラメータ化型以外に捕捉変換を適用した場合、結果は適用前と同じ型です。つまり、java.lang.String型に捕捉変換を適用した場合、結果もjava.lang.Stringとなります。

重要なことは、捕捉変換は式の評価の際に毎回行い、そのたびに新しい型変数を作ってしまう点です。

void f() {
    List<?> list = null;
    tt(list, list);
}
<T> void tt(List<T> a, List<T> b) {}

上記「tt(list, list)」では、listの評価のたびに異なる型変数が捕捉変換によって生成されるため、別の型として判断されます。結果として、ttを呼び出す際の型推論でTを適切に推論することができずに失敗します。

まとめ

*1:リンク貼るのが面倒になった

*2:4.5 Parameterized Types

*3:4.5.1 Type Arguments and Wildcards

*4:4.5.1.1 Type Argument Containment and Equivalence

*5:15.25 Conditional Operator ? :

*6:15.12.2.7 Inferring Type Arguments Based on Actual Arguments, 型推論の正解が分からない - しげるメモ

*7:4.10.2 Subtyping among Class and Interface Types, 4.5.1.1 Type Argument Containment and Equivalence

*8:4.9 Intersection Types