2019/02/06
あるクラスを継承したクラスを書くと、親クラスのメンバ(メソッド及びフィールド)を全て引き継ぐことが出来るっていうところまで、継承って何?で説明しました。
その際、親クラスのメソッドを単純に引き継ぐだけでなく、親クラスのメソッドを子クラスで定義しなおす(上書きする)ことも出来ます。親クラスで定義されているメソッドを子クラスで再定義することをオーバーライドと言います。
よく似たのにオーバーロードっていうのがありますが、全く関係ないので混同しないようにして下さい。
参考:メソッドのオーバーロードって何?
オーバーライドってどういうことなのかよく分かるように、実際にやってみましょう。
sponsored link
オーバーライドをしてみる
こんなCarクラスがあるとします。
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リットルと思っていてください。
メンバメソッドは、
- ガソリンを消費して走るdriveメソッド
- ガソリンを補給するputGasメソッド
- 現在のガソリンの量を出力するcheckGasメソッド
の3つです。
driveメソッドは、引数に渡したガソリンの量に応じて走るものとします。燃費はリッター当たり10kmということにしておきましょう。フィールドのgasの数値は、使った分だけ減ります。
putGasメソッドは、引数に渡した数量だけgasが増えます。給油です。
checkGasメソッドは、現在のgasの値を出力します。
継承の仕組みにスポットを当てる為、あえてカプセル化は不十分な状態になっています。
同一パッケージ内のCarTestクラスでこのCarクラスをインスタンス化して走らせてみましょう。
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クラスを実行すると、標準出力に以下のように出力されます。
ここまでは理解できますでしょうか。
さて、今度はCarクラスを継承したEconomyCarクラスを作ります。EconomyCarクラスは燃費が抜群によくてリッター20km走るとします。
class EconomyCar extends Car{ @Override public void drive(int gas){ this.gas -= gas; System.out.println(gas * 20 + "km走りました"); } }
EconomyCarクラスは、Carクラスを継承しているので、何も書かずともフィールドもメソッドもCarクラスのものを全て引き継いでいます。
driveメソッドのみ処理内容を再定義することで、リッター20km走るようにしています。このように子クラスにおいて処理内容を再定義することをオーバーライドと言います。
このEconomyCarクラスをインスタンス化して走らせてみましょう。
public class CarTest{ public static void main(String[] args){ EconomyCar eCar = new EconomyCar(); eCar.drive(20); eCar.drive(30); eCar.putGas(40); eCar.checkGas(); } }
低燃費が自慢のEconomyCarクラスのインスタンスは、リッター20km走ってますね。
子クラスのインスタンスメソッドを呼べば、子クラスで再定義(オーバーライド)されているメソッドが呼ばれているのが分かると思います。
オーバーライドの仕組み
前ページ及びこのページ冒頭で、継承について「親クラスのメンバを引き継ぐ」という表現をしましたが、実際は、継承とは、引き継いでいるというより「親クラスのインスタンスを内包した状態」と言う方が正しいです。
この考え方は、オーバーライドの仕組みを理解する為に非常に重要です。具体的にどういうことか見ていきましょう。
Carクラスを継承したEconomyCarクラスのインスタンスの内部構造を図で表すとこんな感じになります。
EconomyCarインスタンスの中に、Carインスタンスが内包されていて、親子それぞれのクラスのメンバがその中に存在している状態です。EconomyCarクラスのメンバはご覧のようにdriveメソッドだけです。
一応、メンバの概要だけ再掲しておきます。
class Car{ int gas = 60; public void drive(int gas){ // 省略 } public void putGas(int gas){ // 省略 } public void checkGas(){ // 省略 } }
class EconomyCar extends Car{ @Override public void drive(int gas){ // 省略 } }
この状態で、EconomyCarクラスのインスタンスメソッドを呼ぶ(及びフィールドにアクセスする)とはどういう状態かと言うと、
こういうことです。
子クラスのインスタンスメソッドを外から呼んだ際、そのメソッドが子クラスでオーバーライドされている場合はその子クラスのメソッドが呼ばれます。一方、子クラスでは何ら定義されていないメソッドが呼ばれた場合は、内包している親クラスのメソッドが呼ばれます。
子クラスにてオーバーライドされた親クラスのメソッドは、隠されたような状態になるわけです。
これがオーバーライドの基本的な仕組みです。
- 子クラスのインスタンスは、親クラスのインスタンスを内包している
- オーバーライドとは親クラスのメソッドを子クラスのメソッドが覆い隠している状態
というのを理解して下さい。
フィールドをオーバーライド?
この例の場合、オーバーライドされて書き換えられた部分はdriveメソッドの処理中の数字部分(燃費)だけですね。
EconomyCar.java
@Override
public void drive(int gas){
this.gas -= gas;
System.out.println(gas * 20 + "km走りました");
}
これならnenpi(燃費)というフィールドを作って、driveメソッドでそのnenpiの数値を使うようにしておけば、子クラスでnenpiを書き換えるだけで燃費をコントロールすることが出来そうです。
CarクラスとEconomyCarクラスを以下のようにしてみましょう。
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を続けて走らせます。
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(); } }
実行結果がこちらです↓
残念ながらご覧のように、このやり方では低燃費は実現しません。一体、なぜでしょうか??
継承とフィールド
フィールドも原則的にはメソッドのオーバーライドと同じで、子クラスに同名のフィールドがあればそちらが優先して使われます。先ほどの例に出てきたnenpiフィールドを使った場合は以下のようになります。
しかしこの状態だとEconomyCarのnenpiが20に変わっても、EconomyCarの走行距離は2倍にはなりません。
何故かと言うと、この場合呼ばれるdriveメソッドは親インスタンスのdriveメソッドだからです。
先ほど説明したように、EconomyCarクラスでdriveメソッドをオーバーライドしていない状態で、EconomyCarインスタンスのdriveメソッドを呼ぶということは、内包されている親インスタンスのdriveメソッドが呼ばれることになります。
Carクラスのdriveメソッド内で使われているthis.nenpiは、もちろん自身のインスタンス(Carインスタンス)のnenpiを指します。つまり10です。子クラスがあろうと無かろうと関係ありません。
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走りました"); } // 以下省略 }
なので子クラスでフィールドの値をいくつに書き替えていようとも、この状態では全く関係ないということになります。
親クラスから子クラスのフィールドにアクセスすることは不可能なので、残念ながらこのやり方では低燃費を実現することはできません。
逆に親クラスから子クラスのメンバへはアクセスできません。子クラスにとっては親が存在しないことはありえませんが、親クラスというのは継承されてこそ親となるので、クラス設計の段階で存在するかどうかも分からない子クラスのメンバにアクセスする方法がないのは、ある意味当然と言えます。
ちなみに親クラスが持つフィールドと同名のフィールドを子クラスで宣言することは、オーバーライドとは言わず、「継承によるフィールドの隠蔽」なんていう風に呼ばれていて、実は推奨されていません(実装すること自体は可能ですが)。
上記のように、子クラスでフィールドを宣言したところで、親クラスのメソッドを経由してしまうと全くアクセス不能なので、非常に扱いづらくバグの原因となる可能性が高いからです。
もし非推奨とは言え、無理に子クラスでnenpiフィールドを再宣言するのなら、driveメソッドもオーバーライドして子クラスのdriveメソッドからそのnenpiを触るというやり方にする必要があります。
EconomyCar.java
class EconomyCar extends Car{ int nenpi = 20; //燃費 // gasを消費して走る public void drive(int gas){ this.gas -= gas; System.out.println(gas * this.nenpi + "km走りました"); } }
これでは子クラスにおいて、親クラスと全く同じ内容のdriveメソッドをオーバーライドしていることになります。
結果的にはdriveメソッド内のthisが指し示すインスタンスが変わるので意図通りに動くことは動きますが、どうもやり方が不細工かつ非効率的です。継承を使う意義(親クラスの存在意義)が薄れてしまっています。
しかしながら、nenpiというパラメータ(フィールド)を用意することで、燃費をコントロールするというやり方自体は、設計として決して悪いものではないと思います。
次回は、なんとかしてnenpiフィールドを取り入れた設計を完成させてみましょう。superを使います。
今だけ→転職できなければ全額返金の「エンジニア転職保証コース」
絶対エンジニアになる!→テックエキスパート
フリーランスエンジニアの収入例を見てみる→レバテックフリーランス
コメント
そうですね。コンストラクタを書いてなかったらデフォルトコンストラクタが動いているってことです。
継承とコンストラクタについても書いてますのでまた読んでみてください。
by Nobuo@管理人 2017/04/10 19:54
フィールドをオーバーライド?の項目で
200km走りました
300km走りました
ガソリンを40リットル補給しました
ガソリンは残り50リットルです
400km走りました
300km走りました
ガソリンを40リットル補給しました
ガソリンは残り80リットルです
となったのですがsuperを書かなくてもちゃんと倍になってますよ?
by 匿名 2018/09/01 00:27
ガソリンは残り80リットルですになってるのはmsです
by 匿名 2018/09/01 00:32
[…] 【Java】オーバーライドって何? インターフェースを実装!Javaでimplementsを使う方法【初心者向け】 インタフェースと抽象クラスどっち使ったらいいんだ? […]
by 継承と、インターフェースと。 | IT技術情報局 2018/09/06 19:51
>なので子クラスでフィールドの値をいくつに書き替えていようとも、この状態では全く関係ないということになります。
>親クラスから子クラスのフィールドにアクセスすることは不可能なので、残念ながらこのやり方では低燃費を実現することはできません。
一番かんたんなJavaとあり「これからJavaを始めようという人の為の超入門サイトです。」とあったので勘違いしました。このサイトは、Javaを始めてみたがよくわからずもっと親切丁寧におしえてもらうためのサイトですね。上記の書き方は、それを何度か試した人にしかわからない発言ですし。何度か親クラスから子クラスへアクセスしようとプログラムを書き、「おかしいな おかしいな」と疑問に思った人へのアドバイスですね。初心者は脳みそがあっぷあっぷでこんなレベルまで考えられないです。
by 匿名 2019/08/27 01:21