Java | オブジェクト指向:final クラス

Java Java
スポンサーリンク

final クラスとは何か

final クラスは「継承を禁止したクラス」です。extends して子クラスを作ることができません。目的は、クラスの振る舞いを固定して“改変の余地”をなくし、設計の安全性・予測可能性を高めることにあります。値オブジェクトやユーティリティ、セキュリティ上の重要な型でよく使われます。


なぜ final にするのか(重要ポイントの深掘り)

予測可能性と安全性の確保

継承で振る舞いを差し替えられる余地がないため、インスタンスが常に同じ契約どおりに振る舞います。これにより、バグの原因になりやすい「想定外のオーバーライド」を根本から防げます。特に equals/hashCode/toString のような基盤メソッドを持つ値オブジェクトは final にすることで、等価判定やハッシュ構造での安全性が上がります。

不変オブジェクトとの相性が良い

不変(生成後に状態が変わらない)設計と final クラスは相性抜群です。サブクラスで状態や契約がねじ曲がることを防ぎ、equals/hashCode の一貫性を保てます。スレッドセーフな共有やキャッシュにも安心して使えます。

APIの意図を明確化

「この型は拡張してはいけない」という意図をコードで示せます。利用者は拡張前提にせず、委譲やインターフェースで拡張する設計へ自然に導かれます。API進化の自由度も保ちやすくなります(内部構造変更が子に波及しない)。


よく使う場面

値オブジェクト(ID、Email、金額、座標など)

値で等価を判定する小さな型は final にすると安全で扱いやすくなります。

import java.util.Objects;

public final class Email {
    private final String value;

    public Email(String raw) {
        var v = raw == null ? "" : raw.trim().toLowerCase(java.util.Locale.ROOT);
        if (!v.contains("@")) throw new IllegalArgumentException("invalid email");
        this.value = v;
    }

    public String value() { return value; }

    @Override public boolean equals(Object o) {
        return o instanceof Email e && Objects.equals(value, e.value);
    }
    @Override public int hashCode() { return Objects.hash(value); }
    @Override public String toString() { return value; }
}
Java

ユーティリティクラス(インスタンス不要の機能集)

インスタンス化と継承を禁止して、静的メソッドだけ提供します。コンストラクタは private に。

public final class Strings {
    private Strings() {} // 生成禁止
    public static String normalize(String s) {
        return s == null ? "" : s.trim().replaceAll("\\s+", " ");
    }
}
Java

セキュリティ・整合性が重要な基盤型

認証トークン、設定のスナップショットなど、契約破壊が致命的な型は final にして改変経路を断ちます。


final クラスの設計作法(重要ポイントの深掘り)

不変に寄せる(private final+セッター禁止)

フィールドは private final、検証はコンストラクタで完了させ、変更は新インスタンス返し(with スタイル)で表現します。

public final class Money {
    private final int amount;
    private final String currency;

    public Money(int amount, String currency) {
        if (amount < 0) throw new IllegalArgumentException("amount>=0");
        if (currency == null || currency.isBlank()) throw new IllegalArgumentException("currency");
        this.amount = amount;
        this.currency = currency.trim();
    }
    public int amount() { return amount; }
    public String currency() { return currency; }
    public Money add(Money other) {
        if (!currency.equals(other.currency)) throw new IllegalArgumentException("currency mismatch");
        return new Money(amount + other.amount, currency);
    }
}
Java

防御的コピーで不変を守る

可変の配列やリストを持つときは、受け取り時・返却時に必ずコピーまたは不変ビューにします。

public final class Tags {
    private final java.util.List<String> list;
    public Tags(java.util.List<String> input) {
        var xs = (input == null ? java.util.List.<String>of() : input).stream()
            .map(s -> s == null ? "" : s.trim())
            .filter(s -> !s.isEmpty())
            .toList();
        this.list = java.util.List.copyOf(xs); // 変更不可ビュー
    }
    public java.util.List<String> asList() { return list; }
}
Java

equals/hashCode/toString の整合性

等価に使うフィールド集合を固定し、hashCode は同じ集合で計算します。toString は識別・要約のみを出し、機密や巨大データは載せないのが安全です。


継承できないことの影響と代替手段

モックや拡張が必要なら「インターフェース+委譲」

final クラスは継承できないため、テストで差し替えたい場合はインターフェースを用意し、実装クラスを注入します。拡張は「持つ(委譲)」で構成します。

interface Normalizer { String apply(String s); }

public final class TrimNormalizer implements Normalizer {
    @Override public String apply(String s) { return s == null ? "" : s.trim(); }
}

final class Service {
    private final Normalizer n;
    Service(Normalizer n) { this.n = n; }
    String run(String s) { return n.apply(s); } // 差し替え容易
}
Java

クラス階層を部分的に制御したいなら「sealed」

Java 17+ では sealed クラスで「継承可能な子の限定」を選べます。完全禁止(final)ほど強くないが、拡張範囲を制御できます。

public sealed class Shape permits Rect, Circle {}
public final class Rect extends Shape {}
public final class Circle extends Shape {}
Java

例題で身につける

例 1: final 値オブジェクトの安全な等価とハッシュ

import java.util.*;

public final class ProductCode {
    private final String value;
    public ProductCode(String raw) {
        var v = raw == null ? "" : raw.trim().toUpperCase();
        if (v.isBlank()) throw new IllegalArgumentException("code");
        this.value = v;
    }
    @Override public boolean equals(Object o) {
        return o instanceof ProductCode pc && value.equals(pc.value);
    }
    @Override public int hashCode() { return Objects.hash(value); }
    @Override public String toString() { return value; }
}
Java

HashSet/HashMap で別インスタンスでも同値として安全に扱えます。

例 2: ユーティリティを final+private コンストラクタで固定

public final class Urls {
    private Urls() {}
    public static boolean isHttp(java.net.URI u) {
        return u != null && "http".equalsIgnoreCase(u.getScheme());
    }
}
Java

誤った継承・生成経路を塞ぎ、意図どおりの使い方だけを許します。


つまずきやすいポイントと回避

「拡張したくなる」誘惑に負けない

仕様変更時にサブクラスで回避しようとすると、契約がねじれて長期的に壊れやすくなります。拡張は委譲・戦略パターンへ切り替えるのが安全です。

テストで継承モックを作れない

インターフェースを介して差し替える設計にしておけば、final 実装のままでもモック注入が可能です。

性能のために final を避ける必要はほぼない

JIT が最適化するため、final の有無で大きな差は生じにくいです。まず安全性優先で final を選び、必要ならプロファイルして点で最適化しましょう。


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

final クラスは「継承による契約破壊を防ぎ、予測可能で安全な振る舞いを固定する」ための道具です。値オブジェクトやユーティリティは原則 final、不変(private final+防御的コピー)で設計し、equals/hashCode/toString の整合性を保つ。拡張や差し替えが必要なら、インターフェース+委譲、あるいは sealed で範囲制御。この線引きを徹底すると、壊れにくい API と読みやすいコードが手に入ります。

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