1. 継承における誤り
継承は OOP の基礎の一つですが、初学者が最もつまずきやすいテーマでもあります。ここでは古典的なミスを分解し、その回避方法を学びましょう。
スーパークラスのコンストラクタ呼び出しがない (super(...))
サブクラスを作るときに忘れてはならないのは、基底クラスがコンストラクタによる特定の初期化を要求する場合があるということです。もし基底クラスにデフォルト(引数なし)コンストラクタがないなら、サブクラスのコンストラクタで必ずsuper(...) を使って基底クラスのコンストラクタを明示的に呼び出す必要があります。
誤りの例:
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
// エラー! Animal にはデフォルトコンストラクタがありません
public Dog() {
// super(); // コンパイラは自動で super() を挿入しますが、そのようなコンストラクタは存在しません!
}
}
修正方法:
class Dog extends Animal {
public Dog(String name) {
super(name); // これで問題ありません!
}
}
コメント:
基底クラスに引数付きコンストラクタのみがある場合、コンパイラはデフォルトコンストラクタを自動生成しません。これはコンパイルエラーのよくある原因です。
final クラスを継承しようとする、または final メソッドをオーバーライドしようとする
Java ではクラスやメソッドを final として宣言できます。これは次を意味します:
- そのクラスは継承できません。
- そのメソッドはサブクラスでオーバーライドできません。
誤りの例:
final class Cat {}
// コンパイルエラー!
class Tiger extends Cat {
// ...
}
class Animal {
public final void sleep() {
System.out.println("Zzz...");
}
}
class Dog extends Animal {
// コンパイルエラー!
@Override
public void sleep() {
System.out.println("Dog is sleeping...");
}
}
コメント:
「cannot inherit from final」「cannot override final method」というエラーが出たら、修飾子を確認しましょう!
リスコフの置換原則 (Liskov Substitution Principle) の違反
難しそうに聞こえますが、実際の意味はこうです: サブクラスのオブジェクトは基底クラスのオブジェクトとして振る舞えなければならない。よくあるミスは、メソッドをオーバーライドした結果、新しいクラスが基底クラスから期待される動作をしなくなることです。
例:
class Bird {
public void fly() {
System.out.println("私は飛んでいる!");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("ペンギンは飛べません!");
}
}
何が問題か?
Bird を扱うコードは、どの鳥も飛べると期待します。しかし Penguin を渡すと、プログラムが落ちる可能性があります。
より良い設計:
こうした場合は継承関係を見直すか、インターフェース/コンポジションを検討しましょう。
2. メソッドのオーバーロードに関する誤り (overloading)
オーバーロードとは、同じクラスに同名で引数リストが異なるメソッドを複数持つことです。簡単に見えますが、落とし穴もあります。
オーバーライドのつもりがオーバーロードになってしまう(シグネチャの誤り)
初心者がよくやるのは、基底クラスのメソッドをオーバーライドしたいのに、うっかり引数を変えてしまうこと。結果としてオーバーロードになり、ポリモーフィズムが効きません。
誤りの例:
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
// オーバーライドしたかったのに、オーバーロードになっている!
public void makeSound(String extra) {
System.out.println("Bark! " + extra);
}
}
問題:
dog.makeSound() の呼び出しは親クラスのメソッドを呼び出し、あなたの新しいメソッドは呼ばれません。
dog.makeSound("loudly") はオーバーロード側を呼びますが、ポリモーフィズムは機能しません。
ベストプラクティス:
@Override アノテーションを付けましょう。シグネチャを間違えると、コンパイラがすぐに教えてくれます。
@Override
public void makeSound() { /* ... */ }
オーバーロードの意外な挙動(自動型変換、あいまいな呼び出し)
Java は、複数のオーバーロードに一致しうる場合、あなたの期待とは異なるメソッドを「選ぶ」ことがあります。
public class OverloadDemo {
public void print(int x) {
System.out.println("int: " + x);
}
public void print(double x) {
System.out.println("double: " + x);
}
}
OverloadDemo demo = new OverloadDemo();
demo.print(5); // int: 5
demo.print(5.0); // double: 5.0
demo.print(5L); // long -> double: double: 5.0
問題:
demo.print(5L) を呼ぶと、Java は print(double x) を選びます(long は int よりも double への変換が優先されるため)。
Object、Integer、int を受け取るメソッドがある場合、引数に null を渡すとコンパイルエラー「reference to print is ambiguous」になることがあります。
戻り値の型だけが異なる同名メソッドの定義(コンパイルエラー)
Java では、同名かつ引数リストが同じで、戻り値の型だけが異なるメソッドを 2 つ定義することはできません。
public class Demo {
// コンパイルエラー!
public int foo() { return 1; }
public String foo() { return "hello"; }
}
説明:
オーバーロードのシグネチャは「メソッド名 + パラメータ」です。戻り値の型は考慮されません。コンパイラは、どのメソッドを呼ぶべきか判断できなくなります。
3. ベストプラクティス
継承とオーバーロードでつまずかないために、次の推奨事項に従いましょう。
オーバーロイドするメソッドには常に @Override を付ける
可読性が上がるだけでなく、シグネチャの誤りからも守ってくれます。引数やメソッド名を間違えると、コンパイラがすぐに教えてくれます。
@Override
public void makeSound() {
System.out.println("Bark!");
}
オーバーロードとオーバーライドを明確に区別する
- オーバーライド (override): 親メソッドの振る舞いを変える — シグネチャは一致しなければならない。
- オーバーロード (overload): 同名で引数の異なる新しいメソッドを追加する。
比較表:
| オーバーロード (overloading) | オーバーライド (overriding) | |
|---|---|---|
| 場所 | 同一クラス/継承階層内 | サブクラス |
| メソッド名 | 同じ | 同じ |
| 引数 | 異なる | 同じ |
| 戻り値 | 異なってよい | 同じ/共変戻り値で互換 |
| アノテーション | 不要 | @Override 推奨 |
オーバーロードの乱用は避ける
オーバーロードが多すぎると、コードは読みにくく混乱を招きます。バリエーションが多すぎる場合は、パラメータオブジェクトやビルダーパターンの利用を検討しましょう。
4. 例: ペット管理システム
簡単なペット管理システムを作るとします。基底クラス Pet と、サブクラス Cat、Dog があるとします。
public class Pet {
private String name;
public Pet(String name) {
this.name = name;
}
public void speak() {
System.out.println(name + " はよくわからない音を出します。");
}
}
public class Cat extends Pet {
public Cat(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(getName() + " は「ニャー!」と鳴きます。");
}
// エラー: Pet に getName() メソッドがありません!
}
典型的なミス:
メソッドをオーバーライドしようとして、基底クラスに存在しないメソッドを呼んでしまっています。ゲッターを追加するのがよいでしょう。
public class Pet {
private String name;
public Pet(String name) { this.name = name; }
public String getName() { return name; }
public void speak() { System.out.println(name + " はよくわからない音を出します。"); }
}
これで正しく動作し、ポリモーフィズムを利用できます:
Pet myPet = new Cat("バルシク");
myPet.speak(); // バルシクは「ニャー!」と鳴きます。
5. 継承とオーバーロードの典型的なミス
誤りその1: super(...) の呼び出しを忘れる。
基底クラスのコンストラクタに重要なロジックがあり、それを呼ばないと、プログラムが想定外の動作をする、あるいはコンパイルすら通らないことがあります。
誤りその2: 別のメソッドをオーバーライドしてしまう。
親メソッドの振る舞いを変えたかったのに、似た名前や別の引数で新しいメソッドを追加してしまう。結果として、元のメソッドはそのまま動き、新しいメソッドは誰にも呼ばれません。
誤りその3: final メソッドをオーバーライドしようとする。
Java はそれを許可しません(それで正しいのです)。コンパイルエラーが出たら、final を疑いましょう。
誤りその4: メソッドを過度にオーバーロードする。
calculate メソッドのバリエーションが 10 個もあって、どれが呼ばれるのか自分でも混乱しているなら、リファクタリングを検討すべき時です。
誤りその5: リスコフの置換原則を破る。
サブクラスが基底クラスの意味を変えてしまうと、アーキテクチャ全体が崩れます。たとえば、Shape クラスに getArea() があり、サブクラス BrokenShape が -1 を返すような設計は、奇妙なバグにつながりかねません。
GO TO FULL VERSION