1. メソッドのオーバーライド
現実の世界でも「継承した側」が独特のふるまいをする場面はよくあります。たとえば、すべての動物は鳴き声を出せますが、猫は「ニャー」、犬は「ワン」、そしてプログラマは「うわ、またバグだ!」と言います。プログラミングではこれをメソッドのオーバーライド(override)で実現します。
メソッドのオーバーライドとは、サブクラスが親クラスで既に宣言されているメソッドに対して自分自身の実装を提供することです。つまり、標準の振る舞いを自分用のより具体的なものに「置き換える」ことです。
たとえ話。親クラスを「ボルシチのレシピ」と考えると、オーバーライドは祖母がそこに秘密の隠し味を加えるようなものです。ボルシチはボルシチのままですが、味はそれぞれ異なります。
メソッドをオーバーライドするには、子クラスで親クラスとまったく同じシグネチャ(名前、パラメータ、戻り値の型)を持つメソッドを宣言する必要があります。
例: 動物と鳴き声
class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
class Dog extends Animal {
// makeSound() をオーバーライドする
void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
// makeSound() をオーバーライドする
void makeSound() {
System.out.println("Meow!");
}
}
これで、Dog のオブジェクトを作成して makeSound() を呼び出すと、"Woof!" が出力され、"Some generic animal sound" にはなりません。
コードによるデモ
public class Main {
public static void main(String[] args) {
Animal generic = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
}
}
重要: サブクラスに同じシグネチャのメソッドがない場合は、親クラスのメソッドが使われます。
2. アノテーション @Override: 何のために、どう使うか
Java では、オーバーライドしたメソッドに特別なアノテーション @Override を付けるのが一般的です。これは単なる飾りではなく、有用な道具です。
- コンパイラが検証します。本当に親クラスのメソッドをオーバーライドしているかをチェックします。名前、パラメータ型、戻り値の型を間違えるとコンパイルエラーになります。
- コードの可読性が向上します。他の開発者はすぐに「このメソッドは親クラスをオーバーライドしているのだな」と分かります。
@Override の例
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
もし誤って void makeSond()(タイプミス!)と @Override を付けたメソッドに書いてしまうと、コンパイラは次のように文句を言います: "Method does not override or implement a method from a supertype"。
現在の標準。 @Override を使うのは良い作法であり業界標準です。コンパイラが要求しない場合でも常に付けましょう。そうすることで自分にも同僚にも優しくなります。
3. オーバーライドされたメソッド呼び出しの仕組み
サブクラスのオブジェクトに対してメソッドを呼ぶと、変数が親タイプで宣言されていてもサブクラス側の実装が使われます。
例: ポリモーフィズムの動作
Animal animal = new Dog();
animal.makeSound(); // "Woof!" であり、"Some generic animal sound" ではない
ここでは変数の型は Animal ですが、実際に中にあるのは Dog のオブジェクトです。Java は Dog のオーバーライド版を呼び出すべきだと「理解」します。これがポリモーフィズムです(詳細は次回の講義で扱います)。
4. オーバーライドの制約とルール
メソッドのシグネチャ
- 名前、型、パラメータの順番は親メソッドと一致しなければなりません。
- 戻り値の型は一致するか、共変(親の戻り値のサブタイプ)でなければなりません。たとえば、親が Animal を返し、子が Dog を返すのは許可されます。
アクセス修飾子
- 親より厳しいアクセスレベルにすることはできません。
- 親メソッドが public の場合、オーバーライド後も public である必要があります。
- 親が protected の場合、オーバーライド側は protected か public にできます。
逆にしようとすると、コンパイラは次のエラーを出します: "Cannot reduce the visibility of the inherited method"。
例外
- オーバーライドしたメソッドは、親の宣言にない新たなチェック例外をスローできません。
- 親より少ない例外、またはそのサブタイプをスローすることは可能です。
static、final、private
- static や final として宣言されたメソッド、そしてプライベート(private)なメソッドはオーバーライドできません。
- static は隠蔽(hiding)であり、オーバーライドではありません。
- final はそもそもオーバーライド不可で、Java がそのようなメソッドを保護します。
- private はサブクラスから見えないためオーバーライドできません(同名の新しいメソッドを宣言するだけになります)。
コンストラクタ
コンストラクタは継承されず、オーバーライドもできません。各クラスは独自のコンストラクタを持ちます。
5. 学習用アプリ「動物園」を発展させる
理論を実践に移しましょう。私たちの「動物園」アプリをさらに発展させます。
ステップ 1. 基底クラス Animal
public class Animal {
public void makeSound() {
System.out.println("Some generic animal sound");
}
public void sleep() {
System.out.println("Zzz...");
}
}
ステップ 2. サブクラス Dog と Cat
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
// Dog 専用の追加メソッド
public void fetch() {
System.out.println("Dog brings the stick!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
// Cat 専用の追加メソッド
public void scratch() {
System.out.println("Cat scratches the sofa!");
}
}
ステップ 3. オーバーライドを使う
public class ZooTest {
public static void main(String[] args) {
Animal generic = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
// dog.fetch(); // エラー!Animal 型の変数は fetch() を知らない
// cat.scratch(); // 同様
// ただし、明示的に型を指定すれば:
if (dog instanceof Dog) {
((Dog) dog).fetch(); // Dog brings the stick!
}
if (cat instanceof Cat) {
((Cat) cat).scratch(); // Cat scratches the sofa!
}
}
}
コメント:
メソッド makeSound() はポリモーフィックに動作し、実際のオブジェクトのクラスにあるバージョンが呼び出されます。一方、特有のメソッド(fetch、scratch)は明示的なキャストを通してのみ利用できます。これは継承とオーバーライドの仕組みを理解するうえで重要です。
6. 戻り値型の例(共変戻り値)
オーバーライドしたメソッドの戻り値を、より「狭い」型にしたいことがあります。例えば:
class Animal {
Animal getFriend() {
return new Animal();
}
}
class Dog extends Animal {
@Override
Dog getFriend() { // 戻り値の型は Dog(Animal のサブタイプ)
return new Dog();
}
}
これは共変戻り値の型と呼ばれ、Java(Java 5 以降)で許可されています。
7. @Override を使わないとどうなる?
メソッド名やパラメータをうっかり間違えても、アノテーション @Override が付いていなければ Java は文句を言いません。その結果、オーバーライドではなく新しいメソッドを作ってしまい、期待した挙動は変わりません。
誤りの例
class Dog extends Animal {
// タイプミス: makeSound ではなく makeSoud
void makeSoud() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.makeSound(); // "Some generic animal sound" を出力する
}
}
もし @Override を付けていれば、コンパイラは次のエラーを出してくれたはずです: "Method does not override or implement a method from a supertype"。
8. オーバーライドでよくあるミス
誤り 1: アノテーション @Override を付けない。
これがないと、メソッド名やパラメータを間違えやすくなります。結果としてメソッドはオーバーライドされず、プログラムは期待どおりに動きません。
誤り 2: アクセス修飾子を狭めようとする。
親メソッドが public なのに、protected や private にするとコンパイルエラーになります。
誤り 3: シグネチャの不一致。
パラメータが型だけでも異なれば、それはオーバーライドではなくオーバーロード(overloading)です。
誤り 4: final や static メソッドをオーバーライドしようとする。
Java はそれを許可しません。final はオーバーライドを禁止し、static メソッドはそもそもオーバーライドされず(隠蔽のみ)です。
誤り 5: 互換性のない戻り値型に変更する。
親の戻り値型のサブタイプ(共変)だけを返せます。まったく別の型にはできません。
GO TO FULL VERSION