オーバーライド

チャーシューメンのスープといえば,しょうゆ味に決まっている.そう店主は話す.そこで,チャーシューメンを頼むときに,みそ味で頼んだような場合でも,強制的にしょうゆ味で作るようにしたい.

そこで,ChashuMenクラスで,setSoupメソッドを改めて定義する.

public class ChashuMen extends Ramen
{
    private int chashu;
    ChashuMen()
    {
        System.out.println("チャーシューメン作るよ");
    }
    public void setChashu(int chashu)
    {
        this.chashu = chashu;
        System.out.println("チャーシュー:" + chashu);
    }
    public void checkChashuMen()
    {
        System.out.print("チャーシュー:" + chashu);
        if(soup != Taste.しょうゆ)
        {
            System.out.println("\nしょうゆ味でないチャーシューメンはチャーシューメンではない!");
            System.out.println("しょうゆ味に変えます!");
            soup = Taste.しょうゆ;
        }
    }
    @Override
    public void setSoup(Taste taste)
    {
        this.soup = Taste.しょうゆ;
        System.out.println("スープ:" + this.soup);
    }
}

スーパークラスであるRamenクラスは以下のようになっている.

public class Ramen
{
    protected int katasa;//1:固い,2:普通,3:柔い
    protected int men;//1-10
    protected Taste soup;
    Ramen()
    {
        System.out.println("ラーメンつくるよ");
    }
    public void setMen(int men)
    {
        this.men = men;
        System.out.println("量:" + men);
    }
    public void setKatasa(int katasa)
    {
        this.katasa = katasa;
        System.out.println("固さ:" + katasa);
    }
    public void setSoup(Taste taste)
    {
        this.soup = taste;
        System.out.println("スープ:" + this.soup);
    }
    public void checkRamen()
    {
        System.out.print("量:" + men);
        System.out.print("固さ:" + katasa);
        System.out.println("スープ:" + soup);
    }
}
===普通のチャーシューメン===
ラーメンつくるよ
チャーシューメン作るよ
量:5
固さ:2
チャーシュー:5
スープ:しょうゆ
量:5固さ:2スープ:しょうゆチャーシュー:5

このように,チャーシューメンをオーダーしたとき,みそ味を指定しても,しょうゆ味のチャーシューメンが出てくる仕組みをチャーシューメンクラスに組み込むことができた.

オーバーライドできなくする

継承したクラスで,同じ名称のメソッドを定義することで,オーバーライドしたメソッドを提供することができる.しかし,スーパークラスで定義したメソッドが隠れてしまうことが,適切でない場合がある.これを避けるために,オーバーライドされないようにメソッドを定義することができる. その方法は,修飾子にfinalを定義することである.

しかし,ラーメンの味は,チャーシューが乗っているかどうかによらず,自由に選べるべきではないかとみそラーメンの元祖「元祖札幌味噌ラーメン「味の三平」」の店主は語る(嘘). そんなときには,継承したクラスで,オーバーライドさせないようにスーパークラスでメソッドを定義することができる.そのキーワードは「final」である.setSoupメソッドの修飾子として「final」を追加した様子が以下である.これを追加すると,サブクラスでは,オーバーライドできなくなる.

public class Ramen
{
    protected int katasa;//1:固い,2:普通,3:柔い
    protected int men;//1-10
    protected Taste soup;
    Ramen()
    {
        System.out.println("ラーメンつくるよ");
    }
    public void setMen(int men)
    {
        this.men = men;
        System.out.println("量:" + men);
    }
    public void setKatasa(int katasa)
    {
        this.katasa = katasa;
        System.out.println("固さ:" + katasa);
    }
    final public void setSoup(Taste taste)
    {
        this.soup = taste;
        System.out.println("スープ:" + this.soup);
    }
    public void checkRamen()
    {
        System.out.print("量:" + men);
        System.out.print("固さ:" + katasa);
        System.out.println("スープ:" + soup);
    }
}

RamenクラスのsetSoupメソッドに「final」を設定したときの,ChashuMenクラスの様子は以下である.


setSoupが設定できないとの警告が出ている様子を確認できる.

先に出てきたオーバーロード,ここで学んだオーバーライド,これらは,同じ名称でありながら,状況によってメソッドの挙動を変えることができる方法である. このように,状況に応じて,挙動を変えられるはたらきがあることを,「多態性(ポリモーフィズム)」と呼ぶ.

final

ここで,finalの修飾子について,補足しておこう. finalは,フィールド,メソッド,クラスを定義するときに,指定することができる.そして,機能としては,そのフィールド,メソッド,クラスを変更出来ない状態で定義することができる.メソッドでは,オーバーライドできないようなメソッドとして定義できる.では,フィールドやクラスではどうなるだろうか.

フィールドでのfinal

フィールドでfinalを指定することは,代入した値を固定値として定義することを意味している. したがって,定義するときに値を指定しないと,値を後で変更すること担ってしまうので,定義時に値を指定しなければならない.

finalで指定したsoupフィールドを定義するときに,値を指定しない状態で記入すると,以下の図のように,初期化されていないことがエラーで指摘される.


値を指定して定義すると,問題なく定義が完了する.


finalで定義したフィールドは,固定値として定義するためであると説明した.では,そのフィールドを変更しようとするとどうなるであろうか. まずは,soupフィールドを非finalで定義する.そのフィールドを変更するためのメソッドを定義する.以下のように,問題なく定義できる.

では,ここでsoupフィールドをfinalにしてみるとどうなるだろうか.以下の図のように,変更できないことを指摘される.変更するメソッドを設けるためには,finalをつけてはいけないという解決法が提案されている.finalで通すためには,変更する部分を設けないようにすべきで,変更できるようにするためには,finalではないフィールドにしなければならない.状況に応じて,どちらかを選ぶべきだろう.

クラスでのfinal

次に,クラスを定義するときにfinalを指定すると,どうなるだろうか.みそラーメンにチャーシューを載せて,チャーシューメンをつくることができる.そのために,まず,finalを指定せずにMisoRamenを定義しよう.それを継承してMisoChashuMenを定義する.

しかし,「みそラーメンにチャーシューを載せるなどとはけしからん!みそラーメンはそれで究極で最終系なのだ!」と頑固な店主がいたら,それを知らしめ,みそチャーシューメンを出せないようにしたい.そのために,MisoRamenクラスをfinalを指定して定義する.そうすると,そのMisoRamenを継承しようとするとどうなるだろう.ラブクラス化できないとの指摘があり,継承できない.

このように,フィールド,メソッド,クラスについて,定義したものを変更できないようにするために,finalを使うことができる.これによって,余計な変更をできないようにして,セキュリティを確保することができるともいえる.

上乗せ?

上書きではなく,上乗せという説明をしているのは,なぜだろうか.それは,継承先で同じメソッドを定義しても,継承元のメソッドも残るからである.継承元のメソッドをアクセスするには,「super」というキーワードを使う.

例として,checkRamenメソッドを用いて考える. checkRamenのほかに,checkChashuMenメソッドでシャーシューの数をチェックしているが,checkRamenとcheckChashuMenの2つのメソッドを行うのは面倒である.チャーシューメンであっても,チェックするときには,checkRamenメソッドで済ませられれば分かりやすい.そこで,チャーシューメンのときには,通常のラーメンチェックのほかに,チャーシューもチェックするようにしたい.

その時のChashuMenクラスは以下になる.
public class ChashuMen extends Ramen
{
    private int chashu;
    ChashuMen()
    {
        System.out.println("チャーシューメン作るよ");
    }
    public void setChashu(int chashu)
    {
        this.chashu = chashu;
        System.out.println("チャーシュー:" + chashu);
    }
    @Override
    public void setSoup(Taste taste)
    {
        this.soup = Taste.しょうゆ;
        System.out.println("スープ:" + this.soup);
    }

    public void checkRamen()
    {
        System.out.print("量:" + men);
        System.out.print("固さ:" + katasa);
        System.out.print("スープ:" + soup);
        System.out.print("チャーシュー:" + chashu);
        if(soup != Taste.しょうゆ)
        {
            System.out.println("\nしょうゆ味でないチャーシューメンはチャーシューメンではない!");
            System.out.println("しょうゆ味に変えます!");
            soup = Taste.しょうゆ;
        }
    }

}

checkRamenでチェックしていた項目とさらに,チャーシューとかの項目をまとめて,同じメソッド名で定義した.これで,オーバーライドによって,checkRamenメソッドを上乗せしたわけだ.

しかし,これでは,元のcheckRamenがあるのに,それをコピペしたわけだ.通常のラーメンチェックで新しく何か項目が加わったら,それをコピペする必要が出てきたり,再利用性がよくない.そこで,オーバーライド前,すなわち,継承元のcheckRamenを使えばよい.その時には,「super」キーワードを使うことで,「スーパークラスの」という意味で指定することができる.そう,「this」と似ている.

そうすると,以下のように書き換えることができる.
    public void checkRamen()
    {
        super.checkRamen();
        System.out.print("チャーシュー:" + chashu);
        if(soup != Taste.しょうゆ)
        {
            System.out.println("\nしょうゆ味でないチャーシューメンはチャーシューメンではない!");
            System.out.println("しょうゆ味に変えます!");
            soup = Taste.しょうゆ;
        }
    }

@Override

抽象メソッドをサブクラスで自動的に実装させるときや,インターフェースを自動的に実装するとき, メソッドの上に「@Override」と自動的に追加されることが確認できただろうか. これは,すでに定義されているメソッドをここでオーバーライドしていることを明示するための記号である.この記号をつけることで,ここでオーバーライドしていることが分かりやすくなるほかに,オーバーライドとオーバーロードを間違わないようにするためにも,役立つ.

オーバーライドとオーバーロードを間違わないように役立つとはどういうことだろうか. 例えば,Ramenクラスで定義している「public void setSoup(Taste taste)」メソッドをオーバーライドして,味を指定してもみそ味しか設定できないような制約をかけたいとかんがえる.

以下のようにスーパークラスで定義されているとする.

public abstract class Ramen
{
    protected Taste soup;
    public void setSoup(Taste taste)
    {
        this.soup = taste;
        System.out.println("スープ:" + this.soup);
    }
}

スーパークラスで定義されているメソッドをオーバーライドして,以下のように定義する.

public class ChashuMen extends Ramen
{
    @Override
    public void setSoup(Taste taste)
    {
        this.soup = Taste.しょうゆ;
        System.out.println("スープ:" + this.soup);
    }
}

例えば,オーバーライドするとき,引数を処理としては無視しているので,引数がないメソッドを定義してしまうとどうなるだろうか?


すなわち,以下のようにする.するとどうだろうか.全く問題なく定義できてしまう.すなわち,オーバーライドではなく,オーバーロードしているのである.

public class ChashuMen extends Ramen
{
    public void setSoup()
    {
        this.soup = Taste.しょうゆ;
        System.out.println("スープ:" + this.soup);
    }
}

この状態でスープの味をみそでオーダーすると,以下のように,みそ味のチャーシューメンができてしまう.こうなると,もともとのオーバーライドの目的であった,「『みそ味のチャーシューメン』をオーダーされてもしょうゆ味に強制的に変更する」メソッドにすることが叶えられない結果となる.


そこで,オーバーライドするつもりだったのにオーバーロードしてしまうミスを防ぐために,オーバーライドするメソッドを宣言するときには,その冒頭に「@Override」を明示することで,開発環境が指摘してくれるようになる.例えば,先ほどの,オーバーロードしてしまったメソッドの冒頭に「@Override」を記述すると,以下のように,オーバーライドしていないことを警告するメッセージが表示される.


オーバーライドするつもりであったメソッドと合致するように,引数を与えて,所望の通りにオーバーライドすると,エラーがなくなる.



次の例では,メソッドのほか,フィールドについても同じ名称で定義している.そのような場合にどのような挙動を示すのか,確認する.


Personクラスでは,名前のフィールドと,名前を紹介するメソッドが設定されている.

public class Person
{
    protected String name;
    Person(String name)
    {
        this.name = name;
    }
    public void selfIntroduction()
    {
        System.out.println("私の名前は" + name + "です");
    }
}

Personクラスを継承して,Actorクラスを作る.Actorクラスでは,コンストラクタにて,本名と芸名を設定してインスタンス化することができる.そのとき,Personクラスをインスタンス化するときに,本名を設定してインスタンス化するように設定する. そして,本名は,スーパークラスの変数として設定しておく.

さらに,スーパークラスと同じメソッドとして,selfIntroductionメソッドを設定する. これにより,ActorクラスのselfIntroductionメソッドを呼ぶと,PersonクラスのselfIntroductionではなく,ActorクラスのselfIntroductionが呼ばれる. このように,同じ名前でメソッドを設定することを「オーバーライド」と呼ぶ.

public class Actor extends Person
{
    private String name;
    Actor(String realname, String name)
    {
        super(realname);
        this.name = name;
    }
    public void selfIntroduction()
    {
        System.out.println("私の芸名は" + name + "です");
    }
    public void selfRealIntroduction()
    {
        System.out.println("私の本名は" + super.name + "です");
    }
}

Personクラス及び,Actorクラスの挙動を確かめるために,以下のようにインスタンス化して試してみる.

public class Main
{
    static public void main(String args[])
    {
        Actor yuka = new Actor("岡部広子", "優香");
        yuka.selfIntroduction();
        yuka.selfRealIntroduction();
        Person taro = new Person("太郎");
        taro.selfIntroduction();
    }
}

これを実行すると,以下のような出力が得られる.

私の芸名は優香です
私の本名は岡部広子です
私の名前は太郎です

演習

タンメンクラスについて,タンメンがオーダーされた時には,しょうゆ味が指定されても,塩味で必す調理されるように,オーバーライドせよ. また,みそタンメンをつくることを許し,塩味に限定させないRamenクラスを作るにはどのようにすればよいか,確認せよ.

継承したクラスでメソッドを上乗せする