Java | オブジェクト指向:継承(extends)

Java Java
スポンサーリンク

まず、継承(extends)とは何か

継承は「既存のクラスの性質(フィールドやメソッド)を受け継ぎ、必要な部分だけを差し替えたり足したりする」仕組みです。extends で親クラスを指定すると、子クラスは親の振る舞いを使えますし、同じメソッド名・引数で再定義(オーバーライド)して自分流の動作に置き換えられます。これにより共通の型で扱いつつ、実体ごとに振る舞いを切り替える「ポリモーフィズム」が実現します。

abstract class Shape {
    abstract double area(); // 子が必ず実装する“契約”
}

final class Rect extends Shape {
    private final double w, h;
    Rect(double w, double h) { this.w = w; this.h = h; }
    @Override double area() { return w * h; }
}

final class Circle extends Shape {
    private final double r;
    Circle(double r) { this.r = r; }
    @Override double area() { return Math.PI * r * r; }
}
Java

継承で起きることと動的ディスパッチ(重要)

実体の型でメソッドが選ばれる

変数の型が親でも、実際に入っているインスタンスの型に応じてメソッドが呼び分けられます。これが動的ディスパッチです。共通のインターフェースで扱いつつ、実体ごとに適切な実装が動きます。

Shape s = new Rect(3, 4);
System.out.println(s.area()); // Rect の area

s = new Circle(2);
System.out.println(s.area()); // Circle の area
Java

フィールドや static は“切り替わらない”

インスタンスメソッドは動的に切り替わりますが、フィールド参照や static メソッドは「見えている型」に依存して決まります。動的ディスパッチの対象ではありません。オーバーライドできるのはインスタンスメソッドだけです。


コンストラクタと super の使い方(重要)

親の初期化は必ず先に

子クラスのコンストラクタは、最初の行で親のコンストラクタを呼び出せます(super)。親が状態を正しく初期化する前に子の処理を始めることはできません。

class Base {
    final String name;
    Base(String name) {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("name");
        this.name = name.trim();
    }
}

class User extends Base {
    final boolean active;
    User(String name, boolean active) {
        super(name);       // まず親を初期化
        this.active = active;
    }
}
Java

親の処理を拡張する

子のメソッドから super.method() を呼ぶと、親の処理に前後の追加ができます。テンプレートメソッドやフックの設計でよく使います。

class Base {
    void render() { System.out.println("header"); }
}
class Sub extends Base {
    @Override void render() {
        super.render();            // 親の前処理
        System.out.println("body");// 追加
    }
}
Java

オーバーライドのルールと可視性

互換性のあるシグネチャで上書きする

メソッド名・引数(型と並び)は親と一致が必要です。戻り値は「より具体的な型(サブタイプ)」に狭められます(共変戻り値)。違う引数で同名を定義すると、それは“オーバーロード”であり、上書きにはなりません。@Override を必ず付けるとコンパイル時に間違いを検出できます。

abstract class Factory { abstract Number create(); }
class IntFactory extends Factory {
    @Override Integer create() { return 42; } // 共変戻り値(OK)
}
Java

アクセスは広げられるが、狭められない

親が public のメソッドを、子で protected や package-private に狭めることはできません。契約破壊になります。逆に、package-private を public に広げるのは可能です。

class Base { void run() {} }      // package-private
class Sub extends Base {
    @Override public void run() {}// 広げるのは OK
}
Java

final・static・private は上書き不可

final メソッドはオーバーライドできません。static は“隠蔽”になって動的に切り替わらず、private は子から見えないので上書けません。オーバーライドは「見えているインスタンスメソッド」に限られます。


設計指針:いつ継承を使うか(重要)

「is-a(〜は〜である)」が真のときだけ

Rect は Shape である、Circle は Shape である——このように“本質的に親の一種”といえる場合に継承が適します。機能を使いたいだけなら、継承ではなく委譲(持つ・利用する)にします。

// 委譲の例:Logger を“使う”
final class Service {
    private final Logger logger;
    Service(Logger logger) { this.logger = logger; }
    void run() { logger.info("run"); }
}
Java

カプセル化を壊さない

親の protected フィールドを直接触るより、private フィールド+protected な“検証付きメソッド”で拡張ポイントを提供する方が安全です。内部整合性を親が守れる設計が望ましいです。

abstract class Account {
    private int balance;
    protected Account(int initial) { this.balance = initial; }
    protected final int balance() { return balance; }
    protected final void add(int amount) { balance += amount; }
}
Java

継承は強い結合になる

親の仕様変更が子に波及します。ライフサイクルが長く、変更が多い領域ではインターフェース+委譲の方が柔軟です。値オブジェクトは原則 final にして継承しないのが定石です。


例題で身につける継承の実践

例 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);                      // 必須拡張
}

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)を固定し、差し替えたいポイントだけ protected/abstract で露出します。子は必要箇所だけをオーバーライドすればよく、誤用が減ります。

例 2: 共通型で扱うポリモーフィズム

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

例 3: 継承ではなく委譲が正解のケース

// NG例(継承の乱用)
class CachedHttpClient extends java.net.http.HttpClient { /* 実装詳細に強く依存して破綻しやすい */ }

// OK例(委譲)
final class CachedHttp {
    private final java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient();
    java.net.http.HttpResponse<String> get(String url) throws Exception {
        var req = java.net.http.HttpRequest.newBuilder(java.net.URI.create(url)).GET().build();
        return client.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
    }
}
Java

仕上げのアドバイス(重要部分のまとめ)

継承は「親の契約を守りながら、子で振る舞いを差し替える」道具です。インスタンスメソッドだけが動的に切り替わり、コンストラクタは必ず親を先に初期化します。オーバーライドは同じシグネチャで、戻り値はより具体的に狭めるのは可。final/static/private は対象外。設計では is-a が成立する場面に絞り、テンプレートメソッドで拡張ポイントを最小に、乱用時は委譲へ切り替える——この線引きを守れば、拡張しやすく壊れにくい継承が手に入ります。

タイトルとURLをコピーしました