オーバーライド
機能を拡張することはクラスを継承することで実現できましたが、既存のメソッドの振る舞いを変更したい場合もあります。
そのような時、メソッドのオーバーライド機能を使うことでスーパークラス(基底クラス)に変更を加えずにメソッドの振る舞いを変更することができます。
継承時のメソッド
あるクラスにサブクラス(派生クラス)を定義した場合、スーパークラス(基底クラス)のメソッドはサブクラスに定義されているかのように使うことができました。
public class Foo { public void hoo() { // 処理 } }
public class Bar extends Foo { }
□Barクラスに対しFooクラスで定義されているメソッドが使用可能
Bar bar = new Bar();
bar.hoo();
Barクラスにはメソッドを追加することができますが、この時にFooに定義されているメソッドと全く同じメソッドをBarに再定義することができます。
public class Bar extends Foo { public void hoo() { // 処理2 } }
これはメソッドを上書しているイメージであり、これを(メソッドの)オーバーライドと呼びます。
尚、メソッド名・引数・戻り値のどれか1つでも異なる場合、そのメソッドは別のメソッドでありオーバーライドではありません。
このメソッド名、引数、戻り値のセットはメソッドのシグニチャと呼ばれます。
この時、サブクラスのインスタンスに対しオーバーライドしたメソッドを実行したならば上書きされた処理が行われます。
スーパークラスのインスタンスに対してメソッドを実行したならば、スーパークラスに実装されていた元の処理がが行われます。
Foo foo = new Foo(); // オーバーライドされた処理が実行される foo.hoo(); Bar bar = new Bar(); // 基底クラスの処理が実行される bar.hoo();
実行時にどちらに定義された処理が行われるかはインスタンスの型依存することに注意して下さい。
オーバーライドと継承は密接な関係にあります。
継承したクラスでメソッドを上書きすることがオーバーライドなので当然ですが、重要な点はスーパークラス(基底クラス)に一切変更を加えずにメソッドの振る舞いを変更している点です。
継承を使うことにより既存のクラスにメソッドを拡張できましたが、オーバーライドにより既存クラスの振る舞いの変更も可能なわけです。
変数の型とインスタンスの型
オーバーライドでは既存クラスのメソッドを上書しました。
それではこのようにスーパークラスの型で宣言された変数に対して実行される処理はオーバーライドされたメソッドでしょうか?
Bar bar = new Foo();
bar.hoo();
ポイントはBar型の変数として定義されていますが、生成されたのはFooのインスタンスであることは変わらない点です。
したがって、実行されるのはオーバーライドされたメソッドとなります。
実行時にどちらに定義された処理が行われるかはインスタンスの型依存することを忘れないで下さい。
インスタンスと変数の型が同じであるという固定観念を捨てることが重要です。
メソッドのアクセスレベル
メソッドは自由に定義できるように思えて、実は幾つかのルールが存在します。
そのルールのほとんどはオーバーライドを上手く使う為に定められていると言えます。
privateメソッドはオーバーライドできない
privateメソッドはオーバーライドできません。
なぜならば、privateメソッドは他のクラスから見ることは出来ない、言い換えれば各々のクラスで閉じているからです。
しかし、同じシグニチャを持つprivateメソッドをサブクラスに定義することは可能です。
この場合はオーバーライドしているわけではなく、それぞれ独立したメソッドでしかありません。
アクセスレベルを厳しくできない
スーパークラスで定義されたメソッドのアクセスレベルを厳しくする事はできません。
継承を行うと既存クラスの機能が拡張されより汎用的になるように感じてしまう為、アクセスレベルを緩くする事はできるのではないか?と疑問に思うかもしれません。
しかし、集合的な視点で考えるのであれば拡張するということは機能が特化され汎用的でなくなるということである為、部分集合的な扱いになります。
仮にアクセスレベルを厳しくできるとします。
public class Bar extends Foo { void hoo(){ } }
この時、Barと同一パッケージでないならば次のコードはコンパイルエラーとなります。
Bar bar = new Bar(); bar.hoo(); // コンパイルエラー
何故ならば、hooはBarでpackage privateであるからです。
ところで、BarはFooのサブクラスである為、暗黙的な型変換が可能です。
Bar bar = new Bar();
Foo foo = (Foo) bar;
すると、Fooではhooはpublicである為、次のコードが成立しなくてはなりません。
Bar bar = new Bar();
Foo foo = (Foo) bar;
foo.hoo();
しかし、fooが参照するインスタンスはBarのインスタンスです。
つまり、Barのpackage privateなメソッドを実行できることになります。
これは明らかに矛盾ですので、アクセスレベルを厳しくしてはならないということになります。
逆にアクセスレベルを緩くすることは可能です。
super
オーバーライドを使用することでメソッドの上書が可能となりました。
これは外部から見たならば、スーパークラスのメソッドを隠蔽している、と言えます。
実際に行われる処理はインスタンスの型に依存するだけで、オーバーライドメソッドを実行することはできません。
外部から見た場合、この制限はメリットとなりますが、オーバーライドメソッドを記述する時は不便なケースがあります。
次のプログラムは継承のサンプルで作成した計算機クラスです。
[CalculatorWithTax.java]
package example04; // 計算機クラス public class CalculatorWithTax extends Calculator { // 消費税(5%)を取得する public int getTaxAmount() { int amount = getTotalAmount(); return amount * 0.05; } }
ここでCalculatorクラスのgetTotalAmountをオーバーライドして、消費税込の金額を出力したいと考えます。
package example04; // 計算機クラス public class CalculatorWithTax extends Calculator { // 消費税(5%)を取得する public int getTaxAmount() { int amount = getTotalAmount(); return amount * 0.05; } // 合計金額取得(消費税込) // オーバーライドメソッド public int getTotalAmount() { return totalAmount + getTaxAmount(); } }
すると、totalAmountがprivateである為、コンパイルエラーとなってしまいます。
また、getTaxAmountメソッドではgetTotalAmountを呼び出している為、無限ループが発生してしまいます。
このような時、スーパークラス(基底クラス)のメソッドを明示的に呼び出す仕組みが用意されています。
package example04; // 計算機クラス public class CalculatorWithTax extends Calculator { // 消費税(5%)を取得する public int getTaxAmount() { int amount = super.getTotalAmount(); return amount * 0.05; } // 合計金額取得(消費税込) // オーバーライドメソッド public int getTotalAmount() { return super.getTotalAmount() + getTaxAmount(); } }
このようにメソッドの頭にsuperを記述することで、1つ上位階層にあるクラスのメソッドを明示的に呼び出すことが可能です。
superは自分自身を参照する特殊な変数の1つで予約語となっています。
よってgetTaxAmountではCalculatorから課税前の合計金額を取得できるようになり、消費税が計算できます。
getTotalAmountでもCalculatorの課税前合計金額に、(自分自身の)消費税を加えて合計値を返すことができるようなりました。
尚、完全な自分自身を参照する特殊な変数も存在し、thisを用います。
メソッドの呼び出しの時には、thisは省略することができる為に通常は記述しませんが、次のように書いても同じ事となります。
// 合計金額取得(消費税込) // オーバーライドメソッド public int getTotalAmount() { return super.getTotalAmount() + this.getTaxAmount(); }