合ってたらしい

型が合ったぁぁ - 設計と実装の狭間で。のフォローアップ。

オーバーライドの共変戻り値型と、パラメータ化型のサブタイピングを組み合わせた感じです。

共変戻り値型

8.4.8 Inheritance, Overriding, and Hiding
一言で言うと、オーバーライドしたメソッドは、親クラスのメソッドのサブタイプを返してOK。

interface A {
    Number f();
}

interface B extends A {
    Integer f(); // ok
}

サブタイピング

パラメータ化型が絡むと多少厄介です。ただ、使えると便利なのでぜひ。

まず、演算子の定義。

  • 「U
  • 「U <= V」→ UはVに含まれる

とりあえずはまりやすいポイントは、U <: V のときに A <: A でないという点です。

僭越ながらサンプルを書くと、

public class Main {
  public void main(String[] args) {
    NumberedA np = ...;

    List<NumberedB> fs1 = np.nestedb(); // もしこれがOKだったとき
    List<B<Integer, Long>> fs2 = np.nestedb();

    assert(fs1 == fs2); // fs1, fs2は同一のリストとして

    fs2.add(new B<Integer, Long>()); // fs2(=fs1)にはBが入る
    NumberedB b = fs1.get(0); // fs1(=fs2)から取り出すとNumberedBではなくB<..>
  }
}

といった具合に一貫性が保てなくなります。

これだとジェネリクスが使い物にならないので、Javaでは型の「範囲」を表現するためにワイルドカードが導入されてます。ワイルドカードは型そのものではなく、「この型のサブクラス」とか「この型のスーパークラス」とかいう境界の指定をします。無指定の場合は全部。

パラメータ化型は、型パラメータが「より狭い範囲」を表現しているときにサブタイプとみなすことができます。この「より狭い範囲」を「型の包括」と呼び、U <= V を「UはVに含まれる」といいます。

んで、G< U> <: G< V> が成り立つには、UはVより狭い範囲でなければなりません。サブタイピングが「特化」だとすると、より狭い範囲を指定することは直観的にずれてない感じ。

  1. まず、?はすべての型の範囲を表現しますので、すべての型が含まれます
    • T <= ? (G <: G)
  2. ワイルドカードに境界を指定した場合、その境界型はワイルドカードに含まれます
    • T <= ? extends T (G <: G)
    • T <= ? super T (G <: G)
  3. ワイルドカードにextends(上限)境界を指定した場合、境界型のサブタイプはワイルドカードに含まれます
    • U <: V のとき、 U <= ? extends V (G <: G)
  4. ワイルドカードにextends(上限)境界を指定した場合、サブタイプを境界型に持つワイルドカードは、元のワイルドカードに含まれます
    • U <: V のとき、 ? extends U <= ? extends V (G <: G)

覚えるのもばからしいので、こう考えると楽な感じ。
まず、すべての型を、上限と下限を持つ型の範囲([上限, 下限])と考えてしまいます。

  • はすべての型のサブタイプ
  • Objectはすべての型のスーパータイプ
  • T は [T, T] (? extends T super T) の省略形
  • ? は [Object, ] (? extends Object super ) の省略形
  • ? extends T は [T, ] (? extends T super ) の省略形
  • ? super T は [Object, T] (? extends Object super T) の省略形

このうえで、

  • (? extends U, super V)
    • S

数直線上に並べてみると◎。[V, U] <: [T, S] なので、S〜Tの間にU〜Vが来ています。もちろん、? ([Object, ])が全てを含むことも確認できます。

Object  :>  T  :>  V  :>  U  :>  S  :> <null>
                  <=[V, U]=>
           <========[T, S]========>
<==============[Object, <null>]=============>

本題

やっと前置き終了。
List は List< B> のサブタイプではないので、オーバーライドで戻り値型の指定はできません。

List< ? extends B>にしておくと、

  • NumberedB <: B なので、NumberedB <= ? extends B
    • NumberedB <= ? extends B なので、List <: List>

ただ、List< ? extends B>で指定してしまうと、返されるリストに変更を加えるのが大変になります。読む分にはOK。

イディオム的に。

  • メソッドの戻り値のListに対して、読み書きを行う場合→共変させない(=Listの形でそのままにしとく)
  • メソッドの戻り値のListに対して、変更を行わない場合→List< ? extends T>の形にしとく
  • メソッドの戻り値のListに対して、変更のみを行う場合→List< ? super T>の形にしとく

こうすると、オーバーライドするときに便利です。