Java SE 8 でのメソッド継承のルール

Java SE 8 では、デフォルトメソッドが追加されたため、メソッド継承のルールが新しくなりました。以下に、 Java SE 8 でのメソッド継承のルールを一通りまとめます。

型消去 (type erasure)

「型消去」とは、総称型から型パラメータを引っぺがすことです。たとえば、 <T> List<T> asList(T[] elements) を型消去すると List asList(Object[] elements) になります。

言語仕様では次のように定義されています。

型消去 (erasure, type erasure) とは、パラメータ化された型・型パラメータである可能性がある型から、パラメータ化された型・型パラメータではない型への対応です。型 T の型消去は |T| と書きます。型消去の対応を以下に定義します。

  • パラメータ化された型 (§4.5) G の型消去は |G|
  • ネストした型 T.C の型消去は |T|.C
  • 配列型 T[] の型消去は |T|[]
  • 型変数 (§4.4) *1 の型消去は型変数 *2 のもっとも左側の束縛
  • それ以外の型の型消去は型それ自体

型消去はまた、コンストラクタあるいはメソッドのシグネチャ (§8.4.2) から、パラメータ化された型・型パラメータを持たないシグネチャへの対応も行います。コンストラクタあるいはメソッドのシグネチャ s の型消去は、 s と同じ名前と、 s の全ての仮パラメータの型消去から構成されます。

メソッドのシグネチャが型消去された場合、メソッドの戻り型 (§8.4.5) にも、同様に型消去が適用されます。総称メソッド (§8.4.4) ・総称コンストラクタ (§8.8.4) のシグネチャが型消去された場合、総称メソッド・総称コンストラクタの型パラメータにも、同様に型消去が適用されます。

総称メソッドのシグネチャの型消去に、型パラメータは含まれません。

4.6. Type Erasure @ Java Language Specification

シグネチャ (signature)

シグネチャ」とは、名前と仮パラメータの型の組です。

メソッドあるいはコンストラクタ M, N は、次を満たす場合に「同じシグネチャ」を持つと言います: M と N が同じ名前であり、同じ型パラメータ (§8.4.4) (あれば)を持ち、 N の仮パラメータ (formal parameters) の型を M の型パラメータに変換 (adapt) した後に、同じ仮パラメータを持つ場合。

8.4.2. Method Signature @ Java Language Specification

サブシグネチャ (subsignature)

メソッド m1 のシグネチャは、次のいずれかを満たす場合にメソッド m2 のシグネチャの「サブシグネチャ」であると言います。

8.4.2. Method Signature @ Java Language Specification

煩雑なので、「m1 のシグネチャが m2 のシグネチャのサブシグネチャである」ことを、この記事では「m1 <=< m2」と表します。

たとえば次のプログラムがある時。

class X {
    void m(List<String> list) {}
}

class Y {
    void m(List list) {}
}

次の3つの命題が成り立ちます。

  • Y.m <=< X.m
  • X.m <=< X.m
  • Y.m <=< Y.m

オーバーライド等価 (override-equivalent)

メソッドのシグネチャ m1 が m2 のサブシグネチャである場合、または m2 が m1 のサブシグネチャである場合に限って、 m1 と m2 は「オーバーライド等価」です。

クラスの中にオーバーライド等価のシグネチャを持つメソッドを2つ宣言すると、コンパイル時エラーになります。

8.4.2. Method Signature @ Java Language Specification

前節のプログラムで、 X.m と Y.m はオーバーライド等価です。

継承 (inheritance) の基本

まず具象メソッドの継承について。

次のすべての条件が満たされる場合、クラス C は、直接のスーパークラスから、すべての具象メソッド (concrete methods) m (静的メソッド・インスタンスメソッドとも)を「継承」します。

  • m が C のメンバである
  • m が public あるいは protected であるか、または C と同じパッケージ内でパッケージアクセスとして宣言されている
  • m のシグネチャのサブシグネチャ (§8.4.2) をシグネチャとするようなメソッドが、 C で宣言されていない (a)
8.4.8. Inheritance, Overriding, and Hiding @ Java Language Specification

条件 (a) が満たされないために、スーパークラスのメソッドが継承されない例を示します。次のプログラムでは、 C.a <=< S.a であるようなメソッド C.a が C で宣言されています。そのため、 C は S.a を継承しません。この時、 C.a が S.a を「C からオーバーライドする」と言います。

class S {
    void a(List<String> list) {}
}

class C extends S {
    @Override void a(List list) {}
}

条件 (a) は、「m と同じシグネチャを持つメソッドが C で宣言されていない」と定義する方が自然に見えるはずです(後述の (b), (c) も同様)。わざわざサブシグネチャの概念を使って定義されている理由は、総称が Java に追加された際に、既存のコードとの互換性を壊さずにライブラリを総称化できるようにしたためです。この経緯は次のように説明されています。

サブシグネチャという概念は、2つのメソッドについて、シグネチャが同一でなくても、片方がもう一方をオーバーライドできるような関係を表すために導入されました。これによって、総称型を使わないメソッドが、総称化されたメソッドをオーバーライドできるようになります。これはライブラリの作者にとって重要です。クライアントが、ライブラリの提供するクラスやインタフェースのサブクラスやサブインタフェースを定義している場合でも、クライアントと独立にメソッドを総称化できるからです。

次の例について考えてみてください。

class CollectionConverter {
    List toList(Collection c) {...}
}
class Overrider extends CollectionConverter {
    List toList(Collection c) {...}
}

総称が導入される前に、既に上記のコードが書かれていたとします。ここで、 CollectionConverter の作者がコードを総称化することに決めたとします。すると、次のようになります。

class CollectionConverter {
    <T> List<T> toList(Collection<T> c) {...}
}

仮に特別の考慮(引用者註: 条件 (a))がなかったとすると、 Overrider.toList はもはや CollectionConverter.toList をオーバーライドしなくなり、不正なコードになってしまいます。このような条件では、ライブラリの作者は既存のコードの移行をためらうでしょうから、総称の導入は著しく抑制されてしまいます。

8.4.2. Method Signature @ Java Language Specification

ついで抽象メソッド・デフォルトメソッドの継承について。

次のすべての条件が満たされる場合、クラス C は、直接のスーパークラスおよび直接のスーパーインタフェースから、すべての抽象メソッドおよびデフォルトメソッド (§9.4) m を「継承」します。

クラスはスーパーインタフェースから静的メソッドを継承しません。

8.4.8. Inheritance, Overriding, and Hiding @ Java Language Specification

次のプログラムで、 D.b, D.c, D.d は、それぞれ上述の条件 (b), (c), (d) を満たさないため、 クラス C に継承されません。

interface D {
    void b(List<String> list);
    void c(List<String> list);
    void d(List<String> list);
}

interface D_Dash extends D {
    @Override void d(List list);
}

class S {
    public void c(List list) {}
}

class C extends S implements D, D_Dash {
    @Override public void b(List list) {}
}

コンパイル時エラーになるケース

まず、具象メソッドの継承に関する制限。

クラス C が継承する具象メソッドのシグネチャが、 C が継承する他のメソッドのシグネチャとオーバーライド等価である場合は、コンパイル時エラーです。

8.4.8.4. Inheriting Methods with Override-Equivalent Signatures @ Java Language Specification

たとえば次のプログラムで、 S.m <=< D.m ではないため、条件 (a) は成り立たず、 C は S.m, D.m ともに継承します。しかし、 D.m <=< S.m であり、したがって S.m と D.m はオーバーライド等価であるため、上記の定義に引っ掛かり、コンパイル時エラーになります。

interface D {
    void m(List list);
}

class S {
    public void m(List<String> list) {}
}

// コンパイル時エラー!
class C extends S implements D {
}

ついで、デフォルトメソッドの継承に関する制限。

クラス C が継承するデフォルトメソッドのシグネチャが、 C が継承する他のメソッドのシグネチャとオーバーライド等価である場合は、コンパイル時エラーです。ただし、 C の(引用者註: 直接・間接を問わない)スーパークラスで宣言されている抽象メソッドを C が継承しており、この抽象メソッドが前記2つのメソッドとオーバーライド等価である場合を除きます。

8.4.8.4. Inheriting Methods with Override-Equivalent Signatures @ Java Language Specification

たとえば、次のプログラムはコンパイル時エラーになります。 C が継承しているデフォルトメソッド I1.m が、同様に C が継承している I2.m とオーバーライド等価だからです。

interface I1 {
    default void m() {}
}

interface I2 {
    void m();
}

// コンパイル時エラー!
abstract class C implements I1, I2 {
}

しかしここで、 C のスーパークラス S で、 I1.m, I2.m とオーバーライド等価な抽象メソッド S.m が宣言されていると、コンパイル時エラーは解消されます。この場合、 C は I1.m, I2.m, S.m をすべて継承します。つまり、 S.m が I1.m, I2.m をオーバーライドするわけではありません。

interface I1 {
    default void m() {}
}

interface I2 {
    void m();
}

abstract class S {
    public abstract void m();
}

abstract class C extends S implements I1, I2 {
}

最後に、戻り型についても制限があります。

引用されたメソッドの内どれか1つが、他のすべてのメソッドに対して戻り型置換可能である必要があります。そうでない場合、コンパイル時エラーが起きます(このケースでは throws 節はエラーの原因となりません)。

8.4.8.4. Inheriting Methods with Override-Equivalent Signatures @ Java Language Specification

*1:正しくは "type parameter" のような気がします。

*2:同前。