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; }
}
Javaequals/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; }
}
JavaHashSet/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 と読みやすいコードが手に入ります。
