第23講・STEP 03

オーバーライドって何?

親クラスから引き継いだメソッドは、子クラスで処理を定義し直すことができます。今回は燃費の違う車を例に、「オーバーライド」の仕組みと、フィールドでは同じことができない理由を見ていきましょう。

オーバーライドをしてみる

 親クラスのインスタンスメソッドを、子クラスで定義し直すことをオーバーライドと言います。名前が似ている「オーバーロード」は、引数の型や個数などが違う同名のメソッドを用意することでしたね。今回は、同じ呼び出し方のまま処理を変えます。

 こんなCarクラスがあるとします。各ファイルは同じパッケージに置いてください。

Car.java

class Car{
    int gas = 60; //ガソリン
    
    // gasを消費して走る
    public void drive(int gas){
        this.gas -= gas;
        System.out.println(gas * 10 + "km走りました");
    }
    
    // gasを補給する
    public void putGas(int gas){
        this.gas += gas;
        System.out.println("ガソリンを" + gas + "リットル補給しました");
    }
    
    // 現在のgasの値を出力する
    public void checkGas(){
        System.out.println("ガソリンは残り" + this.gas + "リットルです");
    }
}

 フィールドはgasのみ。ガソリンです。60で初期化しています。60リットルと思っていてください。

 メンバメソッドは、

  1. ガソリンを消費して走るdriveメソッド
  2. ガソリンを補給するputGasメソッド
  3. 現在のガソリンの量を出力するcheckGasメソッド

の3つです。

 driveメソッドは、引数に渡したガソリンの量に応じて走るものとします。燃費はリッター当たり10kmということにしておきましょう。フィールドのgasの数値は、使った分だけ減ります。

 putGasメソッドは、引数に渡した数量だけgasが増えます。給油です。

 checkGasメソッドは、現在のgasの値を出力します。

継承の仕組みに注目するため、今回はフィールドをprivateにせず、入力値や残量のチェックも省いています。走行時には、残量以下の正の整数を渡す前提です。

 同一パッケージ内のCarTestクラスでこのCarクラスをインスタンス化して走らせてみましょう。

CarTest.java

public class CarTest{
    public static void main(String[] args){
        Car car = new Car();
        car.drive(20);
        car.drive(30);
        car.putGas(40);
        car.checkGas();
    }
}

 このCarTestクラスを実行すると、実行結果がコンソールに表示されます。

実行結果
200km走りました
300km走りました
ガソリンを40リットル補給しました
ガソリンは残り50リットルです

 ここまでは理解できますでしょうか。

 さて、今度はCarクラスを継承したEconomyCarクラスを作ります。EconomyCarクラスは燃費が抜群によくてリッター20km走るとします。

EconomyCar.java

class EconomyCar extends Car{
    
    @Override
    public void drive(int gas){
        this.gas -= gas;
        System.out.println(gas * 20 + "km走りました");
    }
}

 EconomyCarクラスは、Carクラスを継承しているので、この例のgasフィールドと3つのメソッドを引き継いでいます。
 driveメソッドのみ処理内容を再定義することで、リッター20km走るようにしています。このように子クラスにおいて処理内容を再定義することをオーバーライドと言います。

 このEconomyCarクラスをインスタンス化して走らせてみましょう。

CarTest.java

public class CarTest{
    public static void main(String[] args){
        EconomyCar eCar = new EconomyCar();
        eCar.drive(20);
        eCar.drive(30);
        eCar.putGas(40);
        eCar.checkGas();
    }
}
実行結果
400km走りました
600km走りました
ガソリンを40リットル補給しました
ガソリンは残り50リットルです

 低燃費が自慢のEconomyCarクラスのインスタンスは、リッター20km走ってますね。

 子クラスのインスタンスメソッドを呼べば、子クラスで再定義(オーバーライド)されているメソッドが呼ばれているのが分かると思います。

オーバーライドの仕組み

 EconomyCarのインスタンスは、Carから引き継いだ状態と、EconomyCarで定義した振る舞いを持つ一つのオブジェクトです。内部に別のCarインスタンスがもう一つ作られる、という意味ではありません。

 今の例では、どのメソッドを呼ぶかによって、使われる定義が次のように決まります。

呼び出し使われる定義
eCar.drive(20)EconomyCarのdrive()
eCar.putGas(40)Carから引き継いだputGas()
eCar.checkGas()Carから引き継いだcheckGas()

 driveは子クラスでオーバーライドされているので、子クラスの定義が使われます。putGasとcheckGasはオーバーライドしていないので、親クラスから引き継いだ定義を使います。どのメソッドも、同じオブジェクトのgasを扱っています。

 ここで大事なのは、親クラスの型の変数に入れても、実体がEconomyCarなら、オーバーライドされたdriveが呼ばれるということです。CarTest.javaのmainメソッド内で試してみましょう。

CarTest.java

Car car = new EconomyCar();
car.drive(20);
実行結果
400km走りました

 変数の型はCarですが、この実体はEconomyCarオブジェクトです。そのため、実行されるのはEconomyCarクラスのdrive()です。

フィールドをオーバーライド?

 この例の場合、オーバーライドされて書き換えられた部分はdriveメソッドの処理中の数字部分(燃費)だけですね。

EconomyCar.java

@Override
public void drive(int gas){
    this.gas -= gas;
    System.out.println(gas * 20 + "km走りました");
}

 これならnenpi(燃費)というフィールドを作って、driveメソッドでそのnenpiの数値を使うようにしておけば、子クラスでnenpiを書き換えるだけで燃費をコントロールすることが出来そうです。

 CarクラスとEconomyCarクラスを以下のように変更してみましょう。EconomyCarのdriveは削除し、親クラスから引き継いだものを使います。CarのputGasとcheckGasは最初の例のままです。

Car.java

class Car{
    int gas = 60; //ガソリン
    int nenpi = 10; //燃費
    
    // gasを消費して走る
    public void drive(int gas){
        this.gas -= gas;
        System.out.println(gas * this.nenpi + "km走りました");
    }
    
    // 以下省略
}

EconomyCar.java

class EconomyCar extends Car{
    int nenpi = 20;
}

 こうすればEconomyCarのnenpiは20になります。これならわざわざdriveメソッドをオーバーライドせずとも、リッター20km走れるようになるんじゃないでしょうか?

 実行してみましょう。以下のようにCarとEconomyCarを続けて走らせます。

CarTest.java

public class CarTest {
    public static void main(String[] args) {
        Car car = new Car();
        car.drive(20);
        car.drive(30);
        car.putGas(40);
        car.checkGas();
        
        System.out.println("");
        
        EconomyCar eCar = new EconomyCar();
        eCar.drive(20);
        eCar.drive(30);
        eCar.putGas(40);
        eCar.checkGas();
    }
}
実行結果
200km走りました
300km走りました
ガソリンを40リットル補給しました
ガソリンは残り50リットルです

200km走りました
300km走りました
ガソリンを40リットル補給しました
ガソリンは残り50リットルです

 残念ながらご覧のように、このやり方では低燃費は実現しません。一体、なぜでしょうか??

継承とフィールド

 実は、フィールドはオーバーライドされません。子クラスで同じ名前のフィールドを宣言すると、親クラスのフィールドを上書きすることにはなりません。親子で同名の別のフィールドとして両方とも存在することになります。上書きしているわけではなく隠しているだけなのでこちらは特にフィールドの隠蔽と呼ばれています。

 つまり、今のEconomyCarのオブジェクトには、Carで宣言したnenpi(10)と、EconomyCarで宣言したnenpi(20)の両方が存在します。

宣言したクラスフィールドと初期値
Cargas = 60、nenpi = 10
EconomyCar別のフィールドとしてnenpi = 20

 この例で呼ばれているdrive()はCarクラス(親)で定義されたメソッドです。その中のthis.nenpiは、Carで宣言した、つまり親側のnenpiを指しているわけです。

Car.java

public void drive(int gas) {
    this.gas -= gas;
    System.out.println(gas * this.nenpi + "km走りました");
}

 この時、thisが指しているのはEconomyCarのオブジェクトです。それでも、フィールドの参照先は、インスタンスメソッドのように実体のクラスに応じて切り替わりません。ここで使われるnenpiは10なので、走行距離は2倍にならないわけです。

 変数からフィールドへ直接アクセスする場合も、参照するフィールドは変数の型によって決まります。次のコードをCarTest.javaのmainメソッド内で実行してみてください。

CarTest.java

EconomyCar eCar = new EconomyCar();
Car car = eCar;  // 同じオブジェクトを参照する

System.out.println(eCar.nenpi);
System.out.println(car.nenpi);
実行結果
20
10

 同じオブジェクトを見ているのに、読み取るnenpiが違いますね。同名のフィールドを二つ持たせると、どちらを使っているのか分かりづらくなるので、通常は避けます。

 もし子クラスのnenpiを使って走らせるなら、EconomyCarでdriveもオーバーライドすることで、次のようには書けます。

EconomyCar.java

class EconomyCar extends Car {
    int nenpi = 20;

    @Override
    public void drive(int gas) {
        this.gas -= gas;
        System.out.println(gas * this.nenpi + "km走りました");
    }
}

 今度のthis.nenpiは、EconomyCarで宣言したnenpiを指すので、20km/Lで走ります。thisが別のオブジェクトに変わったのではなく、メソッドを定義したクラスが変わり、参照するフィールドが変わったんです。

 ただし、親クラスとほとんど同じ処理をもう一度書いてしまっています。これでは処理を直したい時に、両方を直すことになってしまいますね。

 nenpiというフィールドで燃費を管理する考え方自体は悪くありません。次回は、親クラスのメンバを指定するsuperを使って、この設計を見直してみましょう。

目次へ

質問・コメント

読み込み中

この記事についての質問や、学習して気づいたことをお寄せください。

コメントを書く

投稿フォームを準備しています。

コメントと一緒に公開される名前です。
3,000文字以内。パスワードなどの個人情報は書き込まないでください。

コメントは送信後すぐに公開されます。

この記事のコメント

旧サイトに寄せられたコメントも当時の投稿内容のまま掲載しています。現在の記事では変更している内容や、旧環境についてのやり取りも含まれます。

コメントを読み込んでいます。