信頼度ランク
| S | 公式ソース確認済み |
| A | 成功実績多数・失敗例少数 |
| B | 賛否両論 |
| C | 動作未確認・セキュリティリスク高 |
| Z | 個人所感 |
finalキーワードの全パターン — 参照型finalの「中身は変えられる」落とし穴
final変数・finalフィールド・finalメソッド・finalクラスの違いと用途。参照型にfinalをつけたとき「参照は変えられないが中身は変えられる」という最重要の落とし穴を根本から解説。
一言結論
finalは「再代入不可」であって「中身が変わらない」ではない。参照型にfinalをつけると参照(アドレス)は固定されるが、オブジェクトの状態は変更できる。この違いが試験で頻出の落とし穴。
finalには4つの使い方がある
finalは4つの場所で使える。それぞれ意味が微妙に違う:
// 1. ローカル変数
final int x = 10;
// 2. フィールド(インスタンス変数 or クラス変数)
class Foo {
final int value = 5;
static final double PI = 3.14159;
}
// 3. メソッド
class Bar {
final void doSomething() { }
}
// 4. クラス
final class Baz { }
final変数:再代入禁止
final int x = 10;
x = 20; // ❌ コンパイルエラー: cannot assign a value to final variable x
final変数は一度代入したら変更できない。これがfinalの基本的な意味だ。
ローカルのfinal変数:使う前に必ず初期化。宣言と同時でなくてもよい(ただし代入は1回だけ):
final int y; // 宣言だけ(初期化は後で)
y = 10; // ✅ 最初の1回だけOK
y = 20; // ❌ 2回目はエラー
コンパイラは「すべての実行パスで1回だけ代入されているか」を検証する:
final int z;
if (condition) {
z = 1;
} else {
z = 2;
}
// すべてのパスで1回だけ代入 → OK
System.out.println(z); // ✅
finalフィールド:コンストラクタか宣言時に初期化
インスタンスのfinalフィールドは、宣言時かコンストラクタ内で必ず初期化する必要がある:
class Point {
final int x;
final int y = 0; // ← 宣言時に初期化
Point(int x) {
this.x = x; // ← コンストラクタで初期化(OK)
}
Point() {
// x が初期化されていない → ❌ コンパイルエラー
}
}
コンストラクタが複数あるときは、すべてのコンストラクタで初期化しなければならない(直接、またはthis()経由で):
class Box {
final int size;
Box() {
this(10); // ← this()でBox(int)に委譲。Box(int)でsizeが初期化される → OK
}
Box(int size) {
this.size = size;
}
}
static finalフィールドは宣言時かstaticブロックで初期化:
class Config {
static final int MAX;
static {
MAX = calculateMax(); // staticブロックで初期化
}
private static int calculateMax() { return 100; }
}
最重要の落とし穴:参照型のfinal
final List<String> list = new ArrayList<>();
list = new ArrayList<>(); // ❌ 参照の変更はNG(新しいオブジェクトを指せない)
list.add("hello"); // ✅ 中身の変更はOK!
list.add("world"); // ✅
System.out.println(list); // [hello, world]
finalは「変数に格納されているもの(参照型ならアドレス)を変更できない」という意味だ。
参照型変数の中にはアドレスが入っている。finalはそのアドレスを変えられなくする。でもそのアドレスが指すオブジェクトの中身を変えることは制限しない。
final List<String> list → [0x100](アドレス)
↓
ArrayList(["hello", "world"])
list = new ArrayList(); → ❌ 0x100から別のアドレスへの変更はNG
list.add("bye"); → ✅ 0x100番地のArrayListに要素を追加するのはOK
これはfinalの最も誤解されやすいポイントだ。finalがついていても参照型のオブジェクトが不変になるわけではない。
本当に中身まで変えられないようにしたければ、Collections.unmodifiableList()や独自のImmutableクラスを使う:
final List<String> immutable = Collections.unmodifiableList(new ArrayList<>(Arrays.asList("a", "b")));
immutable.add("c"); // ❌ 実行時例外: UnsupportedOperationException
finalメソッド:オーバーライド禁止
class Parent {
final void important() {
// この実装を子クラスが変えることはできない
}
}
class Child extends Parent {
@Override
void important() { } // ❌ コンパイルエラー: cannot override final method
}
finalメソッドは子クラスでオーバーライドできない。「この実装を変えさせたくない」ときに使う。
パフォーマンス上の話として、JVMはfinalメソッドを**インライン展開(inline)**しやすい。メソッド呼び出しのオーバーヘッドを減らせる場合がある(ただし現代のJVMはfinalでなくても最適化するので、パフォーマンスのためにfinalをつける必要はほぼない)。
finalクラス:継承禁止
final class Immutable {
// このクラスを継承することはできない
}
class Sub extends Immutable { } // ❌ コンパイルエラー
finalクラスは継承できない。代表例はStringだ(前の記事で解説した通り)。
他にもInteger, Doubleなどのラッパークラスもfinalだ。イミュータブルであることを保証するためにfinalが使われることが多い。
static finalで定数を作る
public class MathConstants {
public static final double PI = 3.14159265358979;
public static final int DAYS_IN_WEEK = 7;
}
static finalの組み合わせで「クラス定数」が作れる。慣例として定数名はすべて大文字のスネークケース(LIKE_THIS)で書く。
インターフェースのフィールドも暗黙的にpublic static finalだった(前の記事で解説)。
blank final(空のfinal)
宣言時に初期化せずに後で代入するfinalを「blank final」という:
class Config {
final String value; // blank final(宣言時に初期化しない)
Config(String value) {
this.value = value; // コンストラクタで初期化(OK)
}
}
コンストラクタの引数によって定数の値を変えたいときに使うパターンだ。
まとめ
| final の対象 | 意味 |
|---|---|
| ローカル変数 | 一度代入したら再代入不可 |
| インスタンスフィールド | 宣言時またはコンストラクタで初期化必須。その後は再代入不可 |
| staticフィールド | 宣言時またはstaticブロックで初期化必須。その後は再代入不可 |
| メソッド | オーバーライド禁止 |
| クラス | 継承禁止 |
最重要の落とし穴:
final 参照型変数 = 参照(アドレス)の変更が禁止されるだけ
→ オブジェクト自体の変更(add/setなど)は禁止されていない
final ≠ immutable
final は「変数への再代入禁止」
immutable は「オブジェクトの状態変更禁止」