継承のメリットを一言で
継承の最大のメリットは「共通の契約を共有しながら、振る舞いだけを差し替えて再利用できる」ことです。コードの重複を減らし、拡張しやすく、呼び出し側は共通型で扱えるため、全体の設計がシンプルになります。特にポリモーフィズム(多態性)とテンプレートメソッドは、継承の恩恵を最も強く引き出します。
再利用と重複削減(共通の枠組みを親に集約)
共通処理を一箇所で定義して、差分だけ子で書く
親クラスに「変わらない骨格」を置き、子クラスで「変わる部分」だけをオーバーライドします。これにより重複が激減し、変更も親に集約できます。
abstract class Importer {
public final void run(String path) { // 変わらない骨格
var data = load(path);
var normalized = normalize(data);
save(normalized);
}
protected abstract String load(String path); // 変わる部分
protected String normalize(String s) { // 既定の共通処理
return s == null ? "" : s.trim().replaceAll("\\s+", " ");
}
protected abstract void save(String data); // 変わる部分
}
final class CsvImporter extends Importer {
@Override protected String load(String path) { return "a,b,c"; }
@Override protected void save(String data) { System.out.println("save:" + data); }
}
Javaこの形(テンプレートメソッド)だと、仕様変更は親の run/normalize を直すだけで全子クラスに反映され、保守コストが下がります。
ポリモーフィズム(呼び出し側をシンプルにする力)
実体に応じて自動で振る舞いが切り替わる
呼び出し側は「親型の参照」だけ持ち、実際のインスタンスが何であるかに応じて適切な実装が呼ばれます。分岐や instanceof が不要になり、拡張時も呼び出し側を変更せずに機能追加できます。
abstract class Notifier {
public final void sendAll(java.util.List<String> targets) {
for (var t : targets) sendTo(t);
}
protected abstract void sendTo(String target);
}
final class EmailNotifier extends Notifier {
@Override protected void sendTo(String target) { System.out.println("Email -> " + target); }
}
final class SmsNotifier extends Notifier {
@Override protected void sendTo(String target) { System.out.println("SMS -> " + target); }
}
// 呼び出し側は共通型で扱う
Notifier n = new EmailNotifier();
n.sendAll(java.util.List.of("taro@example.com"));
Java新しい Notifier(PushNotifier など)を追加しても、sendAll の呼び出しはそのまま——拡張性が高くなります。
契約の共有(LSP:置換可能性がテスト容易性を高める)
「同じ型として扱える」ことが差し替えを容易にする
親が定めたメソッド契約(引数の前提、戻り値の意味)を子が守ることで、呼び出し側は「親型の契約」だけ理解すれば十分です。テストでは親型のダブル(テスト用の子クラス)に差し替えれば、環境依存の部分を簡単にモック化できます。
abstract class PaymentGateway {
public abstract boolean charge(int amount);
}
final class RealGateway extends PaymentGateway {
@Override public boolean charge(int amount) { /* 実際の外部決済 */ return true; }
}
final class TestGateway extends PaymentGateway {
@Override public boolean charge(int amount) { return amount >= 0; } // テスト用
}
class Service {
private final PaymentGateway gw;
Service(PaymentGateway gw) { this.gw = gw; }
boolean buy(int amount) { return amount >= 0 && gw.charge(amount); }
}
Java本番は RealGateway、テストは TestGateway を渡すだけ。共通の契約があるから置換が安全で、テスト容易性が上がります。
共通仕様の一元化(ガバナンスと安全性)
ルールを親で固定、子に拡張ポイントだけを開く
親が検証・前処理・監査などの安全対策を一元化すれば、子の実装がどれだけ増えても最低限の品質が担保されます。事故は「親の窓口」を固めるほど減ります。
abstract class SecureService {
public final void execute(String user) { // 共通の安全枠
audit("start", user);
doExecute(user); // 拡張ポイント
audit("end", user);
}
protected abstract void doExecute(String user);
private void audit(String phase, String user) {
System.out.println("[AUDIT] " + phase + " by " + user);
}
}
final class TransferService extends SecureService {
@Override protected void doExecute(String user) { System.out.println("transfer by " + user); }
}
Java監査の入れ忘れや前処理の欠落を親で防げるため、品質のムラをなくせます。
APIの拡張容易性(変更時の影響範囲が小さい)
新しい振る舞いの追加が呼び出し側ゼロ変更で可能
共通の親型に対して新しい子クラスを追加するだけで、既存の呼び出し側はそのまま機能を利用できます。分岐追加や呼び出し変更が不要なため、影響範囲が最小化されます。
// 既存の Notifier に新規追加
final class PushNotifier extends Notifier {
@Override protected void sendTo(String target) { System.out.println("Push -> " + target); }
}
// 呼び出し側は new PushNotifier() を注入するだけ。sendAll は同じ。
Java継承で契約を共有しているからこそ、拡張が滑らかになります。
例題で「継承のメリット」を体感する
例 1: 仕様変更の親一括
abstract class Importer {
public final void run(String path) {
var data = load(path);
var normalized = normalize(data); // ここを変えるだけで全子が恩恵
save(normalized);
}
protected abstract String load(String path);
protected String normalize(String s) { return s == null ? "" : s.trim(); }
protected abstract void save(String data);
}
Javanormalize の仕様を「複数空白を1つに」へ変えるなら、親の1箇所だけ修正すればすべての Importer に反映されます。
例 2: 呼び出し側の単純化(分岐削減)
abstract class Shape { abstract double area(); }
final class Rect extends Shape { /* ... */ @Override double area(){ return w*h; } }
final class Circle extends Shape { /* ... */ @Override double area(){ return Math.PI*r*r; } }
double sumArea(java.util.List<Shape> shapes) {
return shapes.stream().mapToDouble(Shape::area).sum(); // 分岐不要
}
JavaRect/Circle の追加・変更に関係なく、sumArea は常に正しく機能します。
仕上げのアドバイス(重要部分のまとめ)
継承のメリットは「共通の契約と枠組みを親に集約し、子で差分を安全に表現できる」こと。重複が減り、仕様変更は親の一点で反映され、呼び出し側は共通型で扱えて分岐が消えます。テストでは置換が容易になり、ガバナンス(検証・監査)も親で統一可能。拡張は子クラス追加だけで呼び出し側を変えずに進められます。
