Java
Ruby
C++
C#
Python3

クラスの継承のオーバーライドについて調べてみました。

More than 1 year has passed since last update.

はじめに

後輩から、Javaのクラス継承について質問されたのですが、そらで即答できず、いろいろ調べてみました。

発端

旧@IT会議室:変数の継承について

読みにくいので抜粋

同名変数がスーパークラスに存在する時に、スーパークラスのメソッドを
実行したら同名変数はスーパークラスを参照しました。
自クラスの同名変数を参照させるにはどのようにすればよいでしょうか?

以下サンプルソースです。
「私は子供です。」と返ってくると思っていたものが「私は親です。」と返ってきました。

※ 1ファイルでコンパイルできるようにアクセス修飾子とクラス名を変更。

Main.java
//実行クラス 
public class Main{
    public static void main(String args[]){
        Child c1 = new Child();
        c1.print(); 
    } 
} 

//スーパークラス 
class Parent{
    private String JIBUN = "親"; 
    public void print(){
        System.out.println("私は" + JIBUN + "です。"); 
    }
} 

//子クラス 
class Child extends Parent{
    private String JIBUN = "子供";
}

こちらをみて、説明読んでもよくわからないとのことでした。

Javaの継承、オーバーロードについて

見た瞬間思ったのが、JIBUNがprivateになっているので他のクラスからは参照できずParentとChildでは別ものだよ?ということでした。

しかし、じゃあ、protected(public)だったらどうなの?という疑問が出てきたので、ちゃんと調べてみました。

以下、テストに利用したソースです。
piza.io

Main.java
//実行クラス 
public class Main{
    public static void main(String args[]){
        Child c1 = new Child();
        c1.print(); 
        c1.print2();
        Parent parent = new Child();
        c1.print(); 
        c1.print2();
    } 
} 

//スーパークラス 
class Parent{
    private String JIBUN1 = "親1"; 
    protected String JIBUN2 = "親2"; 
    protected String JIBUN3 = "親3"; 
    public void print(){ 
        System.out.println("print"); 
        System.out.println("私は" + JIBUN1 + "です。"); 
        System.out.println("私は" + JIBUN2 + "です。"); 
        System.out.println("私は" + JIBUN3 + "です。"); 
    }
//    final public void print2(){ オーバーライドしたくない場合は finalを付けておけばコンパイルエラーになる。
    public void print2(){
        System.out.println("print2"); 
        System.out.println("親は" + JIBUN1 + "です。"); 
        System.out.println("親は" + JIBUN2 + "です。"); 
        System.out.println("親は" + JIBUN3 + "です。"); 
    } 
} 

//子クラス 
class Child extends Parent{
    // @Override // オーバーライドできないためコンパイルエラー
    private String JIBUN1 = "子供1";
    // @Override // オーバーライドできないためコンパイルエラー
    protected String JIBUN2 = "子供2";
    Child() {
        this.JIBUN3 = "子供3";
    }
    @Override // オーバーライドアノテーションで、オーバーライドということを明記
    public void print2(){
        System.out.println("print2"); 
        System.out.println("子供は" + JIBUN1 + "です。"); 
        System.out.println("子供は" + JIBUN2 + "です。"); 
        System.out.println("子供は" + JIBUN3 + "です。"); 
    } 
}

実行結果

print
私は親1です。
私は親2です。
私は子供3です。
print2
子供は子供1です。
子供は子供2です。
子供は子供3です。
print
私は親1です。
私は親2です。
私は子供3です。
print2
子供は子供1です。
子供は子供2です。
子供は子供3です。

結果をまとめると以下になります。

  • フィールドはオーバーライドできない。隠ぺいされる。
  • かといってコンパイラはワーニングを出してくれるわけでもない。びっくり!!
  • メソッドは基本オーバーライドしかできない。(隠ぺいはできない。)
  • メソッドのオーバーライドの制御は、親でfinalを付けるとオーバーライド禁止、継承先で、@Overrideを付けるとオーバーライド以外はエラーになる。
  • よって、クラスを作るときにはオーバーライド予定のないメソッドには必ずfinalを付ける。オーバーライドするメソッドを作るときには必ず@Overrideを付けるのが吉。

C#の継承、オーバーロードについて

実は、もともとC#でオブジェクト指向を覚えたので、こちらのほうが書きやすかったです。

piza.io

今回は、メソッドのオーバーライドと隠ぺいがあるため、print2(オーバーライド),print3(隠ぺい)があります。

  • オーバーライド → 親クラスとして使っていても、継承先のメソッドを利用。(既存の動きが書き換わるため、後から動きを定義することが可能)
  • 隠ぺい → 親クラスとして使った場合は親のメソッド。継承先として使った場合は継承先のメソッド。
Main.cs
using System;

public class Test {
    public static void Main(){
        Child c1 = new Child(); 
        c1.print(); 
        c1.print2(); 
        c1.print3(); 
        Parent parent = new Child(); 
        parent.print(); 
        parent.print2(); 
        parent.print3(); 
    }
}

//スーパークラス 
public class Parent{
    // virtual // オーバーライドできないためコンパイルエラー
    private string JIBUN1 = "親1"; 
    // virtual // オーバーライドできないためコンパイルエラー
    protected string JIBUN2 = "親2"; 
    // virtual // オーバーライドできないためコンパイルエラー
    protected string JIBUN3 = "親3";
    public void print(){ 
        Console.WriteLine("print"); 
        Console.WriteLine("私は" + JIBUN1 + "です。"); 
        Console.WriteLine("私は" + JIBUN2 + "です。"); 
        Console.WriteLine("私は" + JIBUN3 + "です。"); 
    } 
    virtual public void print2(){ 
        Console.WriteLine("print2"); 
        Console.WriteLine("親は" + JIBUN1 + "です。"); 
        Console.WriteLine("親は" + JIBUN2 + "です。"); 
        Console.WriteLine("親は" + JIBUN3 + "です。"); 
    }
    public void print3(){
        Console.WriteLine("print3"); 
        Console.WriteLine("親は" + JIBUN1 + "です。"); 
        Console.WriteLine("親は" + JIBUN2 + "です。"); 
        Console.WriteLine("親は" + JIBUN3 + "です。"); 
    } 
} 

//子クラス 
public class Child : Parent{ 
    // override // オーバーライドできないためコンパイルエラー
    private string JIBUN1 = "子供1";
    // override // オーバーライドできないためコンパイルエラー
    protected string JIBUN2 = "子供2";
    public Child() {
        this.JIBUN3 = "子供3";
    }
    override public void print2(){
        Console.WriteLine("print2"); 
        Console.WriteLine("子供は" + JIBUN1 + "です。"); 
        Console.WriteLine("子供は" + JIBUN2 + "です。"); 
        Console.WriteLine("子供は" + JIBUN3 + "です。"); 
    }
//    override public void print3(){ print3にはvirtualがついていないため、コンパイルエラー
    public void print3(){ // ただの上書き定義
        Console.WriteLine("print3"); 
        Console.WriteLine("子供は" + JIBUN1 + "です。"); 
        Console.WriteLine("子供は" + JIBUN2 + "です。"); 
        Console.WriteLine("子供は" + JIBUN3 + "です。"); 
    } 
}

出力結果

print
私は親1です。
私は親2です。
私は子供3です。
print2
子供は子供1です。
子供は子供2です。
子供は子供3です。
print3
子供は子供1です。
子供は子供2です。
子供は子供3です。
print
私は親1です。
私は親2です。
私は子供3です。
print2
子供は子供1です。
子供は子供2です。
子供は子供3です。
print3
親は親1です。
親は親2です。
親は子供3です。

コンパイルエラー(ワーニング)

Compilation succeeded - 2 warning(s)
Main.cs(49,22): warning CS0108: `Child.JIBUN2' hides inherited member `Parent.JIBUN2'. Use the new keyword if hiding was intended
Main.cs(21,22): (Location of the symbol related to previous warning)
Main.cs(60,17): warning CS0108: `Child.print3()' hides inherited member `Parent.print3()'. Use the new keyword if hiding was intended
Main.cs(36,17): (Location of the symbol related to previous warning)

結果をまとめると以下になります。

  • フィールドはオーバーライドできない。隠ぺいされる。
  • privateフィールドはクラス固有のため、隠ぺいのワーニングはなし。protected以上は隠ぺいされるため、ワーニング表示。
  • メソッドはデフォルトオーバーライドできない。(隠ぺいされる。)
  • メソッドをオーバーライドしたい場合は、virtualを付ける。オーバーライド先でも、overrideを付ける。どちらかが欠けるとエラーかワーニングになり、オーバーライドできない。
  • 基本的に感覚と違う何か変なことをしようとすると怒られる(気がする。)

まあ、その感覚はC#で養われたものなので、個人の感想です。

C++の継承、オーバーロードについて

C++についてはまじめにコーディングしたことがないため、ググりながら見よう見まねです。

こちらも、print2,print3があります。

paiza.io

Main.cpp
#include <iostream>
using namespace std;

//スーパークラス 
class Parent{
    private:
        // virtual // オーバーライドできないためコンパイルエラー
        string JIBUN1 = "親1";
    protected:
        // virtual // オーバーライドできないためコンパイルエラー
        string JIBUN2 = "親2"; 
        // virtual // オーバーライドできないためコンパイルエラー
        string JIBUN3 = "親3";
    public:
        void print() {
            cout << "print" << endl;
            cout << "私は" + JIBUN1 + "です。" << endl;
            cout << "私は" + JIBUN2 + "です。" << endl;
            cout << "私は" + JIBUN3 + "です。" << endl;
        };
        virtual void print2() {
            cout << "print2" << endl;
            cout << "親は" + JIBUN1 + "です。" << endl;
            cout << "親は" + JIBUN2 + "です。" << endl;
            cout << "親は" + JIBUN3 + "です。" << endl;
        }
        void print3() {
            cout << "print3" << endl;
            cout << "親は" + JIBUN1 + "です。" << endl;
            cout << "親は" + JIBUN2 + "です。" << endl;
            cout << "親は" + JIBUN3 + "です。" << endl;
        }
};

//子クラス 
class Child : public Parent{ 
    private:
        string JIBUN1 // override // オーバーライドできないためコンパイルエラー
        = "子供1";
    protected:
        string JIBUN2 // override // オーバーライドできないためコンパイルエラー
        = "子供2";

    public:
        Child() {
            this->JIBUN3 = "子供3";
        };
        void print2() override {
            cout << "print2" << endl;
            cout << "子供は" + JIBUN1 + "です。" << endl;
            cout << "子供は" + JIBUN2 + "です。" << endl;
            cout << "子供は" + JIBUN3 + "です。" << endl;
        }
        // void print3() override { print3にはvirtualがついていないため、コンパイルエラー
        void print3() { // ただの上書き定義
            cout << "print3" << endl;
            cout << "子供は" + JIBUN1 + "です。" << endl;
            cout << "子供は" + JIBUN2 + "です。" << endl;
            cout << "子供は" + JIBUN3 + "です。" << endl;
        }
};

int main(void){
    Child *c1 = new Child();
    c1->print();
    c1->print2();
    c1->print3();
    Parent *parent = new Child();
    parent->print();
    parent->print2();
    parent->print3();
}

実行結果

print
私は親1です。
私は親2です。
私は子供3です。
print2
子供は子供1です。
子供は子供2です。
子供は子供3です。
print3
子供は子供1です。
子供は子供2です。
子供は子供3です。
print
私は親1です。
私は親2です。
私は子供3です。
print2
子供は子供1です。
子供は子供2です。
子供は子供3です。
print3
親は親1です。
親は親2です。
親は子供3です。

コンパイルエラー(ワーニング)は特にありませんでした。

結果をまとめると以下になります。

  • フィールドはオーバーライドできない。隠ぺいされる。
  • メソッドはデフォルトオーバーライドできない。(隠ぺいされる。)
  • メソッドをオーバーライドしたい場合は、virtualを付ける。オーバーライド先ではoverrideがあってもなくてもよい。
  • virtualメソッドをoverrideを書かずにオーバーライドできてしまうので惜しい!

python3の継承、オーバーロードについて

LL言語についてもこの際だから調査。
クラスが後付けっぽい感じのpythonです。

いろいろと前提が異なるため、結構意味が違うソースになってしまいました。
ごめんなさい。
(JIBUN1,JIBUN2はクラスフィールドだったりとか)

こちらも、print2,print3がありますが、書き分けられないので同じです。

paiza.io

main.py
# coding: utf-8
# Your code here!

def main():
    c1 = Child()
    c1.print()
    c1.print2()
    c1.print3()

    parent = Parent()
    parent.print()
    parent.print2()
    parent.print3()

# スーパークラス 
class Parent(object):
    JIBUN1 = "親1"
    JIBUN2 = "親2"
    def __init__(self):
        self.JIBUN3 = "親3"

    def print(self):
        print("print")
        print("私は" + self.JIBUN1 + "です。")
        print("私は" + self.JIBUN2 + "です。")
        print("私は" + self.JIBUN3 + "です。")

    def print2(self):
        print("print2")
        print("親は" + self.JIBUN1 + "です。")
        print("親は" + self.JIBUN2 + "です。")
        print("親は" + self.JIBUN3 + "です。")

    def print3(self):
        print("print3")
        print("親は" + self.JIBUN1 + "です。")
        print("親は" + self.JIBUN2 + "です。")
        print("親は" + self.JIBUN3 + "です。")


# 子クラス 
class Child(Parent):
    JIBUN1 = "子供1"
    def __init__(self):
        super(Parent, self).__init__()
        self.JIBUN3 = "子供3"

    def print2(self):
        print("print2")
        print("子供は" + self.JIBUN1 + "です。")
        print("子供は" + self.JIBUN2 + "です。")
        print("子供は" + self.JIBUN3 + "です。")

    def print3(self):
        print("print3")
        print("子供は" + self.JIBUN1 + "です。")
        print("子供は" + self.JIBUN2 + "です。")
        print("子供は" + self.JIBUN3 + "です。")

if __name__ == '__main__':
    main()

実行結果

print
私は子供1です。
私は親2です。
私は子供3です。
print2
子供は子供1です。
子供は親2です。
子供は子供3です。
print3
子供は子供1です。
子供は親2です。
子供は子供3です。
print
私は親1です。
私は親2です。
私は親3です。
print2
親は親1です。
親は親2です。
親は親3です。
print3
親は親1です。
親は親2です。
親は親3です。

結果をまとめると以下になります。

  • そもそも、フィールド定義がコンストラクタで値を代入。値を代入する際にフィールドがない場合には作ってしまう。アクセス修飾子もない。
  • そもそもダックタイピングなので、メソッドもフィールドもオーバーライドし放題というか、最初にみつかったものを参照するだけ。(みつからなかったら、継承元も探しにいく。)
  • selfでクラス変数にアクセスできるけど、書き換えちゃうとインスタンス変数に変身しちゃうところがやばい!だからといって、クラス名.でアクセスするの面倒。

まあ、感覚的にはわかりやすい気がしますね。

rubyの継承、オーバーロードについて

LL言語でも、rubyはクラスを最初から設計したそうなので参考まで。
rubyちょこっとかじっただけです。

こちらも結構意味が違うソースになっています。

paiza.io

Main.rb
# coding: utf-8
# Your code here!

def main()
    c1 = Child.new
    c1.print
    c1.print2
    Child.print3

    parent = Parent.new
    parent.print
    parent.print2
    Parent.print3
end

# スーパークラス 
class Parent
    @JIBUN1 = "親1"
    @@JIBUN2 = "親2"
    def initialize
        @JIBUN3 = "親3"
    end

    def print
        p "print"
        p "私は" + (@JIBUN1 || '未定義') + "です。"
        p "私は" + (@@JIBUN2 || '未定義') + "です。"
        p "私は" + @JIBUN3 + "です。"
    end

    def print2
        p "print2"
        p "親は" + (@JIBUN1 || '未定義') + "です。"
        p "親は" + (@@JIBUN2 || '未定義') + "です。"
        p "親は" + @JIBUN3 + "です。"
    end

    def self.print3
        p "print3"
        p "親は" + @JIBUN1 + "です。"
        p "親は" + @@JIBUN2 + "です。"
        p "親は" + (@JIBUN3 || '未定義') + "です。"
    end
end


# 子クラス 
class Child < Parent
    @JIBUN1 = "子供1"
    @@JIBUN2 = "子供2"
    def initialize
        super
        @JIBUN3 = "子供3"
    end

    def print2
        p "print2"
        p "子供は" + (@JIBUN1 || '未定義') + "です。"
        p "子供は" + (@@JIBUN2 || '未定義') + "です。"
        p "子供は" + @JIBUN3 + "です。"
    end

    def self.print3
        p "print3"
        p "子供は" + (@JIBUN1 || '未定義') + "です。"
        p "子供は" + (@@JIBUN2 || '未定義') + "です。"
        p "子供は" + (@JIBUN3 || '未定義') + "です。"
    end
end

main

実行結果

"print"
"私は未定義です。"
"私は子供2です。"
"私は子供3です。"
"print2"
"子供は未定義です。"
"子供は子供2です。"
"子供は子供3です。"
"print3"
"子供は子供1です。"
"子供は子供2です。"
"子供は未定義です。"
"print"
"私は未定義です。"
"私は子供2です。"
"私は親3です。"
"print2"
"親は未定義です。"
"親は子供2です。"
"親は親3です。"
"print3"
"親は親1です。"
"親は子供2です。"
"親は未定義です。"

結果をまとめると以下になります。

  • クラスありきで設計しているそうですが、フィールド定義がコンストラクタで値を代入。値を代入する際にフィールドがない場合には作ってしまう。アクセス修飾子もない。すみません。調査不足でした。protected,privateはあるが、メソッドのみ。
  • こちらもダックタイピングでした。
  • @@のクラス変数は継承先から書き換え可能!やばい!でも普通のインスタンスメソッドで参照可能。@のクラスインスタンス変数のほうが普通のイメージに近い。でもインスタンスメソッドで参照できない!!

クラスありきで設計したそうですが、クラス変数、クラスインスタンス変数がどうしてこうなったのか謎です。

まとめ

  • java,C#,C++などの静的型付け言語においては、クラスはメソッドのみオーバーライドが可能。フィールドは隠ぺい。
  • python,rubyなどの動的型付け言語においては、オーバーライドににた仕組みをダックタイピングで実現。そのため、フィールドもメソッドも自由に書き換わる。
  • よく知らない言語について質問された場合はちゃんと調べてから回答したほうがよいでしょう。