1. メソッドのオーバーライド
オーバーライド(overriding)とは、サブクラスで親クラスにすでに存在するメソッドの独自バージョンを記述できる仕組みです。これによって、プログラムの実行時(run-time)に真のポリモーフィズムが機能します。
平たく言えば:
基底クラスにあるメソッドをサブクラスで独自に実行させたい場合、サブクラスで同じシグネチャのメソッドを宣言するだけです。基底型の参照を通してメソッドを呼び出しても、実際のオブジェクトの型にあるメソッドのバージョンが呼ばれます。
身近な例:
動物たちのチームがいて、各自に「makeSound()(鳴き声を出して)」という同じ指示を出すとします。犬は吠え、猫は鳴き、牛はモーと鳴く。コードとしては同じメソッド呼び出しに見えても、結果はそれぞれ違う——これがオーバーライドの魔法です!
オーバーライドの構文
基本例
class Animal {
void makeSound() {
System.out.println("動物が何かしらの音を出す");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("ワン!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("ニャー!");
}
}
ここでは Animal クラスに makeSound() メソッドが定義されています。サブクラスの Dog と Cat はこのメソッドをオーバーライドし、それぞれの実装を提供しています。
アノテーション @Override
Java では、オーバーライドしたメソッドに @Override を付けるのが推奨(よい習慣)です:
@Override
void makeSound() { ... }
これは必須ではありませんが、次の利点があります:
- 本当にオーバーライドしているのか(名前やパラメータのタイプミスではないか)をコンパイラが検査してくれます。
- コードの可読性が向上し、他の開発者にもこのメソッドが親のメソッドをオーバーライドしていることが一目で分かります。
オーバーライドされたメソッドの呼び出し
Animal myDog = new Dog();
myDog.makeSound(); // 出力: ワン!
変数の型は Animal ですが、実際には Dog オブジェクトを指しているため、呼び出されるのは Dog クラスのメソッドです。これが動的(遅延)バインディングです。
2. オーバーライド (overriding) とオーバーロード (overloading) の違い
オーバーロード (overloading)
- 同一クラス内(または同じ階層で「近接」)で行われます。
- メソッド名は同じでも、パラメータ(型・数・順序)が異なります。
- メソッドの選択はコンパイル時に行われます。
void print(int x) { ... }
void print(String s) { ... }
オーバーライド (overriding)
- 異なるクラス間で行われます: スーパークラスに宣言されたメソッドをサブクラスでオーバーライドします。
- メソッド名とシグネチャ(パラメータと戻り値の型)が同一です。
- メソッドの選択は実行時(run-time)に、オブジェクトの実際の型に基づいて行われます。
比較表
| オーバーロード (overloading) | オーバーライド (overriding) | |
|---|---|---|
| 場所 | 同一クラス内 | スーパークラスとサブクラス |
| メソッド名 | 同じ | 同じ |
| パラメータ | 異なる | 同じ |
| 戻り値の型 | 異なってもよい | 一致するか、サブタイプであること |
| 選択タイミング | コンパイル時 | 実行時 (run-time) |
| アノテーション | 不要 | @Override(推奨) |
3. オーバーライドのルール
オーバーライドは強力ですが、厳密なルールが伴います。順に見ていきましょう。
メソッドシグネチャは一致させる
- メソッド名、パラメータの型と順序は、スーパークラスのメソッドと同一でなければなりません。
- 戻り値の型は同一か、共変(親メソッドの戻り値型のサブタイプ)である必要があります。
共変戻り値の例:
class Animal {
Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat reproduce() { return new Cat(); } // OK! Cat は Animal のサブタイプ
}
アクセス修飾子
オーバーライドしたメソッドのアクセス修飾子を、スーパークラスのメソッドより厳しくすることはできません。 親で public なら、サブクラスでも public のままにする必要があります。より制限の強い(protected や private)にはできません。
例:
class Parent {
public void greet() { }
}
class Child extends Parent {
// void greet() { } // エラー! デフォルト(package-private)は public よりアクセスが制限される
@Override
public void greet() { } // OK
}
例外
- オーバーライドしたメソッドは、ベースメソッドで宣言されていない新たな checked 例外を投げることはできません。
- より少ない、または同じ例外を投げることは可能です。
例:
class Parent {
void doWork() throws IOException { }
}
class Child extends Parent {
@Override
void doWork() throws FileNotFoundException { } // OK。FileNotFoundException は IOException のサブタイプ
// void doWork() throws SQLException { } // エラー! SQLException は親で宣言されていない
}
static メソッドはオーバーライドされない
static メソッドはオーバーライドではなく(hidden)隠蔽されます。 サブクラスで同じシグネチャの static メソッドを宣言しても、それはオーバーライドではありません! それはメソッドの隠蔽であり、ポリモーフィズムではありません。
class Animal {
static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
static void info() { System.out.println("Dog"); }
}
Dog.info() の呼び出しは "Dog" を出力しますが、Animal 型の変数経由で呼び出すと Animal.info() が呼ばれます。これはポリモーフィズムではありません!
final メソッドはオーバーライド不可
スーパークラスのメソッドが final の場合、それをオーバーライドしようとするとコンパイルエラーになります。
class Animal {
final void sleep() { }
}
class Dog extends Animal {
// @Override
// void sleep() { } // エラー! final メソッドはオーバーライドできない
}
4. 実用例
オーバーライドがどのように動作するか、そしてオーバーロードとの違いを実際に見てみましょう。
例1: Shape クラスとそのサブクラス
class Shape {
void draw() {
System.out.println("図形を描く");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("円を描く");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("長方形を描く");
}
}
ポリモーフィズムを活用する:
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // 円を描く
s2.draw(); // 長方形を描く
}
}
変数は Shape として宣言されていますが、実際のオブジェクトのクラスにあるメソッドが呼び出されます。
例2: オーバーロードとの違い
class Printer {
void print(String s) {
System.out.println("文字列: " + s);
}
void print(int n) {
System.out.println("数値: " + n);
}
}
どちらのメソッドも print という同名ですが、パラメータが異なります——これはオーバーロードであり、オーバーライドではありません。
5. オーバーライドと親メソッドの呼び出し(super)
オーバーライドしたメソッド内で、まず親のロジックを実行し、その後に自分の処理を追加したい場合があります。そのときに使うのが super です。
class Animal {
void makeSound() {
System.out.println("動物が音を出す");
}
}
class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // 親メソッドの呼び出し
System.out.println("ワン!");
}
}
new Dog().makeSound() の出力:
動物が音を出す
ワン!
動的バインディング(late binding)の仕組み
基底型の参照を通してメソッドを呼び出すと、Java は実行時にその参照が指す実際のオブジェクトの型を確認し、そのクラスで定義されたメソッドのバージョンを呼び出します。
Animal a = new Cat();
a.makeSound(); // Animal.makeSound() ではなく Cat.makeSound() が呼ばれる
これが Java におけるポリモーフィズムの要です。
6. あなたのアプリケーションとの関係
学習用アプリ(例えば従業員管理システム)では、基底クラス Employee に work() メソッドを用意し、サブクラスの Manager と Developer がそれぞれ独自にこのメソッドを実装できます:
class Employee {
void work() {
System.out.println("社員が働いている");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("マネージャーが指揮する");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("開発者がコードを書く");
}
}
これで、すべての従業員を同じ配列やリストに格納できます:
Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
e.work(); // 各オブジェクトに応じた出力!
}
7. オーバーライドでよくある誤り
ミスその1: メソッド名やパラメータのタイプミス。 名前やパラメータを誤ると、オーバーライドではなく新しいメソッドを作ってしまい、ポリモーフィズムが機能しません。だからこそ常に @Override を付けましょう——コンパイラがすぐに指摘してくれます。
ミスその2: より厳しいアクセス修飾子。 親でメソッドが public の場合、子クラスでそれを protected や修飾子なしにするとコンパイルエラーになります。
ミスその3: static や final メソッドをオーバーライドしようとする。 static メソッドはオーバーライドされず、final メソッドはそもそもオーバーライドできません。試みるとコンパイラに止められます。
ミスその4: 互換性のない戻り値型への変更。 サブクラスの戻り値型がスーパークラスの型と一致せず、かつそのサブタイプでもない場合、コンパイラはオーバーライドを許可しません。
ミスその5: 新しい checked 例外の追加。 オーバーライドしたメソッドは、ベースメソッドにない checked 例外を新たにスローできません。行うとコンパイルエラーになります。
ミスその6: super を忘れる。 オーバーライドしたメソッドで親の振る舞いを残したい場合は、明示的に super.methodName() を呼び出してください。Java が自動でやってくれることはありません。
これで、メソッドのオーバーライドがどのように機能し、オーバーロードとどう違い、そしてそれによって Java のポリモーフィズムがどのように実現されるかが分かりました。次の講義では、コレクションや配列、実際の課題でポリモーフィズムをどう活用するかを見ていきます!
GO TO FULL VERSION