Ch6. 継承とポリモーフィズム

final修飾子

final修飾子はそれ以上の継承を認めなくする修飾子で、メソッドに設定した場合はオーバーライドが、クラスに設定した場合はサブクラスの作成が出来なくなる。

class FinalClass{
    public final void method(){};
}

class FinalChileClass extends FinalClass{
    public void method(){};
    // ''method()' cannot override 'method()' in 'FinalClass'; overridden method is final
}

super()を介した継承元のコンストラクタの呼び出し方

そもそも継承関係にあるクラスのコンストラクタは、必ず親クラス→子クラスの順で自動で呼び出される。 なので、コンストラクタは親子で存在する場合は必ず両方が呼び出されることになる。

public class Chapter6 {
    public static void main(String[] args){
        FinalChileClass finalChileClass = new FinalChileClass();
        //'FinalClassConstractor'
        //'FinalChildClassConstractor'
    }
}
class FinalClass{
    public final void method(){};
    FinalClass(){
        System.out.println("FinalClassConstractor");
    }
}
class FinalChileClass extends FinalClass{
    //public void method(){};
    // ''method()' cannot override 'method()' in 'FinalClass'; overridden method is final
    FinalChileClass(){
        System.out.println("FinalChildClassConstractor");
    }
}

イメージとしては子クラスのコンストラクタの冒頭に暗黙的にsuper()が導入されているように考えることが出来る。 注意すべきなのは暗黙的に付与されるのはsuper(引数)ではなく、super()であり、子クラスのコンストラクタで引数をとっていたとしても、 継承元には明示的に呼ばない限り 引き継がれない。

また子クラスのコンストラクタでは必ずsuper()が実行されてしまうため、引数なしコンストラクタの代わりに(またはコンパイラによるコンストラクタ作成に頼る場合)、或いは親クラスに引数を取るコンストラクタを実装している場合は暗黙的なsuper呼び出しがエラーになってしまう。 親クラスに引数を取るコンストラクタを1つでも実装している場合はsuper(引数として、明示的に引数付きコンストラクタを呼び出さないといけない。

この問題を解決するにはthis()を利用したオーバーロードしたコンストラクタの呼び出し方と同様、super()を利用して継承先のメソッドを呼び出すことが出来る。 ただし、this()と同様に コンストラクタの先頭に付与しなくてはならない ことに注意する。

ちなみにthis()とsuper()は両方書くことが出来ない(どちらもコンストラクタの先頭に記す必要があるため)。

抽象クラス

Javaでは抽象クラスであっても具象メソッド(実装を持つメソッド)を持つことが出来る。

抽象クラスで注意すべきなのは、 抽象メソッドを定義したときは{}で空の実装を持たせないように 気を付ける必要がある。 また、抽象クラスを継承した抽象クラスでは、必ずしも継承元で定義した抽象メソッドを実装する必要はない。

public class Chapter6 {
    public static void main(String[] args){

        ImplementClass implementClass = new ImplementClass();
        implementClass.method1();
        //'implements!'
        //'implemeht on abstract class'
    }
}
abstract class AbstractClass{
    abstract void method1();
    public void method2(){
        System.out.println("implemeht on abstract class");
    };
}

abstract class AbstractChildClass extends AbstractClass{
//上位の抽象クラスの内容が実装されていなくても問題ない。

class ImplementClass extends AbstractChildClass{
    void method1(){
        System.out.println("implements!");
        this.method2();
    }
}

インターフェース

Java SE7ではインターフェースには抽象メソッドとstaticな変数しか宣言できなかった。(C#の規約のようなもの) Java SE8からインターフェースにも実装を持たせられるようになったことで、RubyのMixinのようなことに挑戦する人が一定数いるようだ。

Mixin 的な実装の継承を実現する: Ruby module, Java interface, PHP trait - Qiita

ただし、インスタンス変数を持たせられる訳ではない。持たせられるのはstaticな変数だけになる。 実際には 宣言した変数は強制的にpublic static outの修飾子がつく ことになる。メソッドに関しても同様で強制的にpublic abstractとなる。

これは、インターフェースの本来の存在意義が【外部に提供する機能の仕様書】であるからだ。 java - Javaのinterfaceは、何故protected修飾が出来ないのか - スタック・オーバーフロー

インターフェースで実装を持たせることは可能だが、publicなメソッドに限られる。

interface NewInterface{
    int a = 100; //OK (強制的にpublic static finalがつく)
    // int b; NG(強制的にfinal宣言となるため、これは出来ない。)
    void methodR(); //OK 強制的にpublic abstractになる。
    default void methodP() {
        System.out.println("World");
    }//OK 強制的にpublicになる
}

class InterfaceImplement implements NewInterface{
    InterfaceImplement(){
        System.out.println(a);
        // a += 100; NG(Cannot assign a value to final variable 'a')
    }
    public void methodR(){
        System.out.println("Hello");
        this.methodP();
    };
}

インターフェースは複数指定することが出来るので、三角継承問題が発生する。 これに関しては、コンパイル時にチェックが走るようだ。

interface InterfaceB {
  default void methodHello(){
    System.out.println("Hello World Again!");
  }
}

interface InterfaceA {
  default void methodHello(){
    System.out.println("Hello World!");
  } 
}

class NewClass implements InterfaceA, InterfaceB{
  //'Duplicate default methods named methodHello with the parameters () and () are inherited from the types InterfaceB and InterfaceA'
}

インターフェースに設定したstatic変数はいろんな呼び方が出来る。インターフェース名で呼ぶことも、実装したクラス名で呼ぶことも、そのまま呼ぶこともできる。

public class Chapter6 {
    public static void main(String[] args){
        InterfaceImplement iI = new InterfaceImplement();
        System.out.println(iI.a);//インスタンスからも呼べる
    }
}

interface NewInterface{
    int a = 100; //OK (強制的にpublic static finalがつく)
    // int b; NG(強制的にfinal宣言となるため、これは出来ない。)
    void methodR(); //OK 強制的にpublic abstractになる。
    default void methodP() {
        System.out.println("World");
    }//OK 強制的にpublicになる
}

class InterfaceImplement implements NewInterface{
    InterfaceImplement(){
        System.out.println(a);//そのままでも呼べる
        System.out.println(NewInterface.a); //実装するインターフェースからでも呼べる
        System.out.println(InterfaceImplement.a);//
        // a += 100; NG(Cannot assign a value to final variable 'a')
    }
}

基本データ型の暗黙型変換

Stringchar[]のパラメータ渡しに互換性はない。

public class Chapter6_3 {
    static void method(char[] param) {
        System.out.println(param);

    }
    public static void main(String[] args) {
        // TODO 自動生成されたメソッド・スタブ
        method("HelloWorld");
    }//型 Chapter6_3 のメソッド method(char[]) は引数 (String) に適用できません
}

オブジェクトの型変換

サブクラスのオブジェクトはスーパークラスの変数でも扱える。 なお、Interfaceの変数とかでもよい。ただし、Interfaceの変数で扱う場合は【Interfaceで宣言されたメソッド】しか呼び出せない。

public class Chapter6 {
    public static void main(String[] args){
        NewInterface object = new InterfaceImplement();
        object.methodP();
        object.methodR();//Interfaceで宣言されているメソッドではないのでコンパイルできない。
        //((InterfaceImplement) object).methodR();//Castしても呼び出せない。
   InterfaceImplement object2 = (InterfaceImplement) object; //下位クラスの変数に代入すると使うことが出来る。
        object2.methodR();
    }
}

interface NewInterface{
    //void methodR(); 
    default void methodP() {
        System.out.println("World");
    }//OK 強制的にpublicになる
}

class InterfaceImplement implements NewInterface{
    public void methodR(){
        System.out.println("Hello");
        this.methodP();
    };
}

Javaコンパイラは、代入された変数のクラス(とその上位のクラス)に宣言されたメソッドだけを探しに行くので、例えオブジェクト自身から見てアクセス可能なメソッドであってもコンパイラがそれを許さない。 コンパイラのメソッド探索経路は上向きだけなのだ。(※実行時の挙動については別途後述)

同様に、オブジェクト自体も一度上位クラスへのキャストを行ってしまうと下位クラスのメソッドにはアクセスできなくなる。

public class Chapter6_3 {

    public static void main(String[] args) {
        // TODO 自動生成されたメソッド・スタブ
        ParentA parentalObject = new ParentA();
        parentalObject.methodParent();          //A: 'Parental Method Call'

        //子→親
        ChildA  childObject    = new ChildA();
        ((ParentA)childObject).methodParent();  //B: 'Parental Method Call'
        //((ParentA)childObject).methodChild(); //C: コンパイルエラー

        //子→親→子
        ParentA castedParent = ((ParentA)childObject);
        ((ChildA)castedParent).methodChild();   //D: 'Child Method Call'

        //親→子
        ((ChildA)parentalObject).methodChild(); //F: Exception in thread "main" java.lang.ClassCastException: ParentA cannot be cast to ChildA at Chapter6_3.main(Chapter6_3.java:15)
        ChildA casted = (ChildA)parentalObject; //G: Exception in thread "main" java.lang.ClassCastException: ParentA cannot be cast to ChildA at Chapter6_3.main(Chapter6_3.java:16)
    }
}

class ParentA{
    public void methodParent() {
        System.out.println("Parental Method Call");
    }
}

class ChildA extends ParentA{
    public void methodChild() {
        System.out.println("Child method Call");
    }
}

以上の例では

  • Bでは子→親のキャストを実施して親クラスのインスタンスメソッドを呼び出している。【OK】
  • Cでは子→親のキャストを実施して子クラスのインスタンスメソッドを呼び出している。【NG】
  • Dでは子→親→子の三段キャストを実施して子クラスのインスタンスメソッドを呼び出している。【OK】
  • E/Fでは親→子のキャストを実施しているが、キャストの時点でランタイムエラーが発生している。【NG】

Bはキャストを実施しなくても可能な操作であり、キャストによってはじめて実現可能な操作はD(子→親→子)のみである。

故に、参照型のキャストは

「あるクラスをインスタンス化」→「インスタンス化されたオブジェクトのスーパークラスやインターフェースの型に代入」→「元クラスに戻す」という場面で使用される。 (教科書原文ママ

また、上位クラスへのキャストが行われると(子クラスの情報が失われ)子クラスのインスタンスメソッドにはアクセスできなくなる。 オブジェクトの型変換については下記のようなお作法がある。

キャスト演算子は以下のようには使用できない。

       ((ParentA)childObject)  //1              
        (ParentA)childObject.methodParent(); //2

1ではキャスト後に代入する変数が指定されていない。 キャスト演算子を使用する際は必ず代入する変数を用意するか、メソッドチェーンでキャスト後のインスタンスを利用する必要がある。 2ではchildObjectに対するメソッド呼び出しが行われた後にキャストを行おうとするため、キャスト後の戻り値に対するキャストとなりエラーになる。

メソッドをオーバーライドした場合

ここまで見てきた様に、子→親のキャストを実施した際は子クラスのインスタンスメソッドがアクセス出来なくなり、親クラスのインスタンスメソッドのみのアクセスが有効になる。 これは、コンパイルの際に

呼び出そうとしているメソッドが宣言されている(変数の型の)クラスに定義されているかどうかを確認

するためである。一方実行時は

インスタンス化されているオブジェクトのメソッドが呼び出される。

そのため、親メソッドを子メソッドがオーバーライドしている場合、コンパイラは親クラスにメソッドの存在を確認する。 一方実行時はインスタンスのメソッドを呼び出すため、子クラスのメソッドが呼び出されることになる。

ただし、フィールドに関しては事情が異なる。フィールドは(オーバライドされていても)コンパイル時と実行時で同じ挙動となる。 インスタンスが代入されている変数が親クラスであれば、子クラスに同名のフィールドが存在しても親クラスのフィールドが呼び出される。

下記コードのその挙動を確認する。

public class Chapter7 {

    public static void main(String[] args) {
        // TODO 自動生成されたメソッド・スタブ
        Fruit object1 = new Fruit();
        Fruit object2 = new Apple();

        System.out.println("Method Call: " + object1.method() + " Field: " + object1.description);
        //'Method Call: Parent Method Call Field: this is Fruit'
        System.out.println("Method Call: " + object2.method() + " Field: " + object2.description);
        //'Method Call: Child method Call Field: this is Fruit'
        Fruit casted = (Fruit)object2;
        System.out.println("Method Call: " + casted.method() + " Field: " + casted.description);
        //'Method Call: Child method Call Field: this is Fruit'

    }
}

class Fruit{
    String description = "this is Fruit";

    public String method(){
        return "Parent Method Call";
    }
}


class Apple extends Fruit{
    String description = "this is Apple";

    public String method() {
        return "Child method Call";
    }
}

3番目の例からわかるように、子→親へのキャストを実施した後も子クラスで親クラスのメソッドのオーバーライドを実施している場合は子クラスのメソッドが呼ばれている。 コンパイラは親クラスに同名のメソッドが存在することを確認してエラーを出さないが、実行時はオブジェクトのメソッドを子クラスから順番に見ていくため、最初に子クラスのメソッドが呼ばれている。

Rubyに存在したメソッド(とフィールド)の探索経路の話はJavaでは必ずしも通用しない。下記のRubyのコードと同じようなコードをJavaで書いたとき、表示はParentになる。

class Parent
  def method
    puts @variable
  end
  
  def initialize()
    @variable = "Parent"
  end
end

class Child < Parent
  def initialize()
    @variable = "Child"
  end
end


a = Child.new()
a.method() #Child

上位クラスが具象クラスの場合のキャスト

ここまでは上位クラスがインターフェースだったり、抽象クラスである場合を見てきたが、上位クラスも具象クラスである場合の型変換について確認する。

子クラスでインスタンス化されたオブジェクトを親クラスのオブジェクトへキャストすることは当然可能である。 また、キャストしなくても親クラスの変数に代入可能であることをこれまで見てきた。

一方、親クラスでインスタンス化されたオブジェクトを子クラスのオブジェクトへキャストしようとするとコンパイルには成功してしまう。 が、実行時に例外を吐く。

class Super{
}
class Child extends Super{
}
class Other{
}

public class Chapter6 {
    public static void main(String[] args){
        Super sp = new Super();
        System.out.println(sp instanceof Child);//'false'
        System.out.println(sp instanceof Super);//'true'
        Child ct = (Child)sp;//親クラスのオブジェクトを子クラスに
        //'class Super cannot be cast to class Child (Super and Child are in unnamed module of loader 'app')
        // at Chapter6.main(Chapter6.java:11)'

        Child ct2 = new Child();
        Super sp2 = (Super)ct2;//子クラスのオブジェクトを親クラスに
        sp2       = ct2;
   }
}

なお、継承関係が全くないオブジェクトに対するキャストはコンパイルの時点で失敗するようになっている。 instanceofに関しても同様で、全く継承関係のないオブジェクト-クラス間の比較はコンパイルに失敗する。

なお、右辺にインターフェースが来る場合、全く継承関係のない(=実装していない)関係であってもコンパイルエラーとはならない点に注意。

AutoBoxingとUnboxing

当たり前に感じていたが、基本データ型とラッパークラスのオブジェクトに間では暗黙の型変換が行われている。 * ラッパークラス→基本データ型:Unboxing * 基本データ型→ラッパークラス:Inboxing

【基本データ型の暗黙型変換】の節で見るように、基本データ型同士では暗黙の型変換が行われるが、Autoboxing×暗黙の型変換は機能しない。

class Other{
    Integer number = 100;//OK: AutoBoxing
    double  number2= 100;//OK: 基本データ型同士の暗黙型変換
    Double  numebr3= 100;//NG: 基本データ型の型変換×AutoBoxing
}

オーバーロードされたメソッド呼び出しのAutoBoxingについても確認する。

class Super{
    int number = 100;
    double doubleNumber = 200;
    Double doubleInstance = (double)300;
    // Double instanceB = new Integer(100); NG:ラッパークラスの暗黙の型変換
    Super(){
        this.method1(number);//'method2'(型の完全一致int-int)
        this.method1((int)doubleNumber);//'method2'(型の完全一致int-int)
        //this.method1((Integer)doubleNumber); NG: 引数内でAutoBoxing×暗黙の型変換を行おうとしている
        //this.method1(doubleNumber); NG: 基本データ型のdouble→intでは暗黙の型変換がない
        //this.method1(doubleInstance); NG: ラッパークラスなので引数が型変換されない。
    }
    void method1(Integer number){
        System.out.println("method1");
    }
    void method1(int number){
        System.out.println("method2");
    }
}

またラッパークラスのインスタンス同士では暗黙の型変換が行われないことも確認できる。 基本データラッパークラス間の互換性 | ITedite

共変戻り値

メソッドのオーバーライドをする際、AutoBoxingによる引数の暗黙の型変換は利用できない。 つまり、下記はコンパイルエラーになる。

abstract class ParentA{
    abstract public void parentMethod1(Integer number);
    abstract public void parentMethod2(int number);
}

class ChildA extends ParentA{
    public void parentMethod1(int number) {}
    public void parentMethod2(Integer number) {}
}

オーバーライドする際は、シグネチャ【メソッド名】【引数の数】【引数の型】【引数の順番】は完全に一致していなくてはならない。 その一方で、戻り値には親クラスで定義されたメソッドで設定した戻り値のサブクラスも設定できる。

継承関係とフィールド

Rubyでは上位のメソッドからでも下位のインスタンス変数にアクセス出来たがJavaではそれは出来ないようだ。 以下のコードの場合、コンパイルが出来ない。

class NewParent{
    public void main() {
        System.out.println(this.number);
    }
}

class NewChild extends NewParent{
    int number = 100;
}