Javaのfinalとは、「これ以上変えられない」ことをコンパイラに約束する修飾子です。変数に付けると再代入できなくなり、メソッドに付けるとオーバーライドできなくなり、クラスに付けると継承できなくなります。 付ける場所によって「何が変えられなくなるのか」が違うので、場所ごとに分けて覚えるとすっきり理解できます。

この記事では、付ける場所別の効果と、そのとき出るコンパイルエラー文を一覧にしました。さらに「final な List に add できるのはなぜか」「static final の定数を書き換えたのに前の値のまま動くのはなぜか」「ラムダ式で出る『事実上のfinal』エラーの直し方」まで、すべて OpenJDK 21.0.10 で実行した出力つきで説明します。Java 25 で変わった点は、JDK 25.0.4.1 で動かせたものは出力を載せ、それ以外は公式ドキュメントで確認した内容として注記します。

finalの早見表

まずは「どこに付けると何が固定されるか」と、ルールに反したときのエラー文です。エラー文は javac 21.0.10 の日本語表示と英語表示の両方を載せています。

付ける場所 書き方の例 固定されるもの 反したときのエラー(日本語/英語)
ローカル変数 final int max = 10; 再代入 final変数maxに値を割り当てることはできません/cannot assign a value to final variable max
フィールド private final int size; 再代入(コンストラクタで1回だけ代入) 同上。代入し忘れは「変数idは初期化されていない可能性があります」/variable id might not have been initialized
引数 void show(final String name) メソッド内での再代入 finalパラメータnameに値を代入することはできません/final parameter name may not be assigned
static フィールド(定数) static final int MAX_RETRY = 3; 再代入 static final変数MAXに値を割り当てることはできません/cannot assign a value to static final variable MAX
メソッド final void run() {} オーバーライド Subのrun()はBaseのrun()をオーバーライドできません(オーバーライドされたメソッドはfinalです)/overridden method is final
クラス final class Leaf {} 継承 final Leafからは継承できません/cannot inherit from final Leaf
コンストラクタ final E17() {} 付けられない 修飾子finalをここで使用することはできません/modifier final not allowed here
abstract と併用 abstract final class X {} 併用できない 修飾子abstractとfinalの組合せは不正です/illegal combination of modifiers: abstract and final

もう1つ、最初に押さえておきたい点があります。

よくある疑問 答え
final な List に add できる? できる。固定されるのは「変数がどのオブジェクトを指すか」で、オブジェクトの中身ではない
中身も変えられなくしたい List.of(...)/List.copyOf(...) など変更不可コレクションを使う
定数はどう書く? public static final int MAX_RETRY = 3;(大文字のスネークケース)
final と書いていないのにラムダで「final が必要」と言われる ラムダで使うローカル変数は「一度も再代入していない」ことが条件(実質的final)
finally・finalize と関係ある? 名前が似ているだけで別物。finally は例外処理、finalize は廃止予定のメソッド

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java finalの意味を変数・メソッド・クラス別に解説|定数の書き方」

動作確認の環境とコマンド

この記事のコードは、次の環境で実行しました。

項目 内容
主に使った JDK OpenJDK 21.0.10(Ubuntu 24.04)
Java 25 の確認 OpenJDK 25.0.4.1(flexible constructor bodies の節のみ)
実行方法 java ファイル名.java(単一ファイル実行)または javac → java
エラー文の言語 日本語は javac -J-Duser.language=ja、英語は javac -J-Duser.language=en で表示

エラー文は OS の言語設定によって日本語か英語のどちらかで出ます。検索するときは英語のエラー文のほうが情報を見つけやすいので、上の表では両方を並べました。

変数にfinalを付ける(再代入できなくなる)

ローカル変数:一度入れた値を変えない

ローカル変数に final を付けると、最初に入れた値から変えられなくなります。

public class E1 {
    public static void main(String[] args) {
        final int max = 10;
        max = 20;
    }
}

コンパイル結果(javac 21.0.10):

E1.java:4: エラー: final変数maxに値を割り当てることはできません
        max = 20;
        ^
エラー1個

英語表示では error: cannot assign a value to final variable max です。「この変数は途中で変わらない」と読む人に伝えられるのがローカル変数の final の役割です。長いメソッドの中で、うっかり別の値を入れてしまう書き間違いをコンパイル時に見つけられます。

for 文のカウンタに付けると、i++ が再代入にあたるのでエラーになります。

for (final int i = 0; i < 3; i++) { }
E16.java:3: エラー: final変数iに値を割り当てることはできません
        for (final int i = 0; i < 3; i++) { }
                                     ^

一方、拡張 for 文(for (final String n : names))は繰り返しのたびに新しい変数が作られるので、final を付けられます。

フィールド:コンストラクタで1回だけ代入する

フィールドに final を付けると、オブジェクトが作られた後は値が変わらないことが保証されます。宣言時に値を入れるか、コンストラクタの中で1回だけ代入します。

public class E2 {
    final int size = 3;
    void grow() { size = 4; }
    static void show(final String name) { name = "x"; }
}
E2.java:3: エラー: final変数sizeに値を割り当てることはできません
    void grow() { size = 4; }
                  ^
エラー1個

ここでは 3 行目のフィールドへの代入だけが報告されています。引数 name への代入は javac の別の段階で検査されるためで、4 行目だけを取り出してコンパイルすると、次のように引数専用の文面でエラーになります。

E19.java:2: エラー: finalパラメータnameに値を代入することはできません
    static void show(final String name) { name = "x"; }
                                          ^

英語表示は error: final parameter name may not be assigned です。

引数:メソッドの中で別の値に差し替えない

引数の final は、メソッドの中でその引数に別の値を入れられなくするものです。呼び出し側には何の影響もありません(呼び出し側から見たメソッドの型も変わりません)。「引数は受け取ったまま使う」という書き方をチームでそろえたいときに使われます。

変数の種類 final の効果 代入できるタイミング
ローカル変数 再代入できない 宣言時、または使う前に1回
インスタンスフィールド 再代入できない 宣言時・初期化ブロック・コンストラクタのどれかで1回
static フィールド 再代入できない 宣言時・static 初期化ブロックのどちらかで1回
引数 メソッド内で再代入できない 呼び出し時に渡された値のまま
catch の引数、try-with-resources の変数 再代入できない(後者は書かなくても final 扱い) —

final Listにaddできる理由(参照は固定・中身は可変)

final について最もよく出る疑問がこれです。final が固定するのは「変数がどのオブジェクトを指しているか(参照)」であり、指している先のオブジェクトの中身ではありません。

flowchart LR
    V["final List tasks(変数)"] -->|"この矢印が固定される"| O["ArrayList オブジェクト"]
    O --> E1["要素: 設計"]
    O --> E2["要素: レビュー"]
    O -. "add / remove で増減できる" .-> E3["要素: テスト"]

実際に動かして確かめます。

import java.util.ArrayList;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) {
        final List<String> tasks = new ArrayList<>();
        tasks.add("設計");          // 中身の変更はできる
        tasks.add("レビュー");
        tasks.remove("設計");
        System.out.println(tasks);

        final int[] scores = {70, 80};
        scores[0] = 100;            // 配列の要素も書き換えられる
        System.out.println(scores[0]);

        final StringBuilder sb = new StringBuilder("A");
        sb.append("B");
        System.out.println(sb);

        // 中身も固定したいときは List.of / List.copyOf
        final List<String> fixed = List.of("設計", "レビュー");
        try {
            fixed.add("テスト");
        } catch (UnsupportedOperationException e) {
            System.out.println("List.of に add: " + e);
        }
        List<String> copy = List.copyOf(tasks);
        tasks.add("テスト");
        System.out.println("元=" + tasks + " コピー=" + copy);
        List<String> view = java.util.Collections.unmodifiableList(tasks);
        tasks.add("公開");
        System.out.println("ビュー=" + view);
    }
}

実行結果(java ListDemo.java、Java 21.0.10):

[レビュー]
100
AB
List.of に add: java.lang.UnsupportedOperationException
元=[レビュー, テスト] コピー=[レビュー]
ビュー=[レビュー, テスト, 公開]

final な List にも add・remove ができ、final な配列の要素も書き換えられました。一方で、変数そのものに別のリストを入れようとするとコンパイルエラーになります。

final List<String> tasks = new ArrayList<>();
tasks = new ArrayList<>();
ListErr.java:5: エラー: final変数tasksに値を割り当てることはできません
        tasks = new ArrayList<>();
        ^

「変えられない」の2つの段階

やりたいこと 使うもの add したとき
変数を別のオブジェクトに付け替えさせない final add できる
リストの中身を変えさせない List.of(...)、List.copyOf(list) 実行時に UnsupportedOperationException
元のリストの「読み取り専用の窓」を渡す Collections.unmodifiableList(list) 窓からは add できないが、元を変えると窓にも反映される
両方固定する final List<String> x = List.of(...) 付け替えも中身の変更もできない

実行結果の最後の2行に注目してください。List.copyOf はその時点の内容を写し取るので、元のリストに後から追加しても影響を受けません。Collections.unmodifiableList は元のリストを覗く窓なので、元に追加した「公開」がそのまま見えています。用途に合わせて選ぶとよいでしょう。

定数の書き方(static final)

Javaで定数を作るときは、static と final を組み合わせます。static は「クラスに1つだけ」、final は「再代入できない」という意味で、合わせて「クラスに1つだけあり、変わらない値」になります。

import java.util.List;
import java.util.Map;

public class Consts {
    public static final int MAX_RETRY = 3;
    public static final String APP_NAME = "TaskBoard";
    public static final double DISCOUNT_RATE = 0.10;
    public static final List<String> ROLES = List.of("ADMIN", "MEMBER");
    public static final Map<String, Integer> LIMITS = Map.of("free", 5, "pro", 50);
    private static final String[] RAW = {"x", "y"};   // 配列は中身を書き換えられる

    private Consts() {}   // インスタンスを作らせない

    public static void main(String[] args) {
        System.out.println(MAX_RETRY + " " + APP_NAME + " " + DISCOUNT_RATE);
        System.out.println(ROLES + " " + LIMITS.get("pro"));
        RAW[0] = "changed";
        System.out.println("配列定数: " + RAW[0]);
        try { ROLES.set(0, "GUEST"); } catch (UnsupportedOperationException e) {
            System.out.println("List.of の定数は変更不可: " + e.getClass().getSimpleName());
        }
    }
}

実行結果(Java 21.0.10):

3 TaskBoard 0.1
[ADMIN, MEMBER] 50
配列定数: changed
List.of の定数は変更不可: UnsupportedOperationException

定数の命名と書き方のルール

項目 書き方 例
名前 大文字とアンダースコア(UPPER_SNAKE_CASE) MAX_RETRY、APP_NAME
修飾子の順番 public static final の順が一般的 public static final int MAX = 1;
複数の値を持つ定数 配列ではなく List.of/Map.of List.of("ADMIN", "MEMBER")
定数だけを集めたクラス コンストラクタを private にする private Consts() {}
種類を表す定数(状態・区分) int 定数より enum が読みやすい enum Status { OPEN, DONE }
インターフェースの中のフィールド 何も書かなくても public static final interface Conf { int MAX = 5; }

配列を定数にすると、要素は誰でも書き換えられる点は覚えておきたいところです(上の実行結果の「配列定数: changed」)。複数の値を定数にするなら List.of を使うと、中身まで固定できます。

static final のフィールドは、宣言時か static 初期化ブロックで値を入れます。どちらも忘れるとエラーになります。

public class E15 {
    static final int LIMIT;
}
E15.java:2: エラー: 変数LIMITは初期化されていない可能性があります
    static final int LIMIT;
                     ^

コンパイル時定数のインライン化に注意する

static final の定数には、知っておくと役立つ性質があります。基本型または String で、値がリテラルなどの定数式で初期化された final 変数は「コンパイル時定数」と呼ばれ、使う側のクラスファイルに値そのものが埋め込まれます(インライン化)。

次の2つのファイルで確かめます。

// Config.java
public class Config {
    public static final int MAX_ITEMS = 10;                 // コンパイル時定数
    public static final String VERSION = "1.0";             // コンパイル時定数
    public static final Integer BOXED = 10;                 // 定数式ではない
}
// App.java
public class App {
    public static void main(String[] args) {
        System.out.println("MAX_ITEMS=" + Config.MAX_ITEMS);
        System.out.println("VERSION=" + Config.VERSION);
        System.out.println("BOXED=" + Config.BOXED);
    }
}

手順は「両方コンパイルして実行 → Config.java の値を 99・"2.0"・99 に書き換えて Config だけ再コンパイル → 実行 → 両方再コンパイル → 実行」です。

$ javac Config.java App.java && java App
MAX_ITEMS=10
VERSION=1.0
BOXED=10

$ javac Config.java && java App      (値を書き換えて Config だけ再コンパイル)
MAX_ITEMS=10
VERSION=1.0
BOXED=99

$ javac Config.java App.java && java App   (両方再コンパイル)
MAX_ITEMS=99
VERSION=2.0
BOXED=99

Config だけ再コンパイルした段階では、MAX_ITEMS と VERSION は書き換える前の値のままで、Integer 型の BOXED だけが新しい値になりました。javap -c App でバイトコードを見ると理由がわかります。

 3: ldc           #15                 // String MAX_ITEMS=10
11: ldc           #23                 // String VERSION=1.0
19: getstatic     #25                 // Field Config.BOXED:Ljava/lang/Integer;

MAX_ITEMS と VERSION は App.class の中に「10」「1.0」という値として書き込まれていて、実行時に Config を見に行っていません。BOXED は getstatic で実行時に Config から読んでいます。

宣言 コンパイル時定数? 使う側に埋め込まれる?
static final int MAX = 10; はい 埋め込まれる
static final String V = "1.0"; はい 埋め込まれる
static final int SUM = 2 * 5; はい(定数式) 埋め込まれる
static final Integer BOXED = 10; いいえ(ラッパー型) 埋め込まれない
static final long T = System.nanoTime(); いいえ(メソッド呼び出し) 埋め込まれない
static final List<String> L = List.of("a"); いいえ(オブジェクト) 埋め込まれない

対応はシンプルで、定数を変えたら使う側もまとめて再コンパイルすることです。 Maven や Gradle でクリーンビルドすれば全体がそろいます。別々の jar に分かれていて、定数側の jar だけ差し替える運用があるなら、定数をメソッド経由(static int maxItems() { return 10; })で公開すると、埋め込みが起きません。

コンパイル時定数であることが必要な場面もあります。旧来の switch の case ラベルがその例です。

static final int OPEN = 1;
static final Integer BOX = 3;
...
switch (s) {
    case OPEN -> System.out.println("open");
    case BOX -> System.out.println("box");
SwitchConst.java:10: エラー: 定数式が必要です
            case BOX -> System.out.println("box");
                 ^

int の OPEN は case に書けますが、Integer の BOX は定数式ではないのでエラー(英語:constant expression required)になります。アノテーションの引数も同じくコンパイル時定数が必要です。

blank finalとコンストラクタ

宣言時に値を入れない final 変数を blank final(ブランクファイナル) と呼びます。フィールドの blank final は、すべてのコンストラクタの終わりまでに、必ず1回だけ代入する必要があります。

public class Member {
    private final String name;      // blank final(宣言時は空)
    private final int rank;

    public Member(String name) {
        this(name, 1);              // 別のコンストラクタに任せる
    }

    public Member(String name, int rank) {
        if (rank >= 3) {
            this.rank = 3;          // 分岐のどちらを通っても1回だけ代入
        } else {
            this.rank = rank;
        }
        this.name = name;
    }

    @Override
    public String toString() { return name + "(rank=" + rank + ")"; }

    public static void main(String[] args) {
        System.out.println(new Member("佐藤"));
        System.out.println(new Member("鈴木", 7));

        final String label;         // ローカル変数の blank final
        if (args.length > 0) {
            label = args[0];
        } else {
            label = "指定なし";
        }
        System.out.println(label);
    }
}

実行結果(java Member.java):

佐藤(rank=1)
鈴木(rank=3)
指定なし

コンパイラは「どの経路を通っても、ちょうど1回代入されるか」を調べています(確実な代入、definite assignment と呼ばれます)。0回でも2回でもエラーです。

状況 コード エラー(日本語/英語)
代入し忘れ(0回) final int id; E5() { } 変数idは初期化されていない可能性があります/variable id might not have been initialized
2回代入 this.id = id; this.id = id + 1; 変数idはすでに代入されている可能性があります/variable id might already have been assigned
代入前に読む final int x; System.out.println(x); 変数xは初期化されていない可能性があります

「可能性があります」という言い回しは、コンパイラが実行せずに経路だけを見て判断しているためです。if の片方だけで代入している、try の中だけで代入している、といったときにもこのエラーが出ます。すべての分岐で1回ずつ代入する形に直すと通ります。

Java 25:super() より前にフィールドを初期化できる

Java 25 では JEP 513(Flexible Constructor Bodies)が正式機能になり、コンストラクタの super(...) より前に文を書けるようになりました。final フィールドを super() の前に代入できます。

public class Flex {
    static class Base {
        Base() { show(); }
        void show() {}
    }
    static class Child extends Base {
        final int size;
        Child(int size) {
            this.size = size;     // Java 25: super() より前にフィールドを初期化できる
            super();
        }
        @Override void show() { System.out.println("size=" + size); }
    }
    public static void main(String[] args) { new Child(5); }
}

Java 21.0.10 でコンパイルした結果:

Flex.java:10: エラー: superの呼出しはコンストラクタの先頭文である必要があります
            super();
                 ^

OpenJDK 25.0.4.1 で実行した結果:

size=5

親クラスのコンストラクタがオーバーライドされたメソッドを呼ぶ場合でも、final フィールドに値が入った状態で動きます。Java 21 で同じことをするには、親のコンストラクタからオーバーライド可能なメソッドを呼ばない設計にするのが基本です。

実質的final(ラムダ式・匿名クラス)

ラムダ式や匿名クラスの中から外側のローカル変数を使うとき、その変数は final、または実質的final(effectively final) である必要があります。実質的finalとは、final と書いていないが、初期化の後に一度も再代入されていない変数のことです。javac の日本語メッセージでは「事実上のfinal」と表示されます。

エラーを再現する

public class E7 {
    public static void main(String[] args) {
        int count = 0;
        Runnable r = () -> System.out.println(count);
        count++;
        r.run();
    }
}
E7.java:4: エラー: ラムダ式から参照されるローカル変数は、finalまたは事実上のfinalである必要があります
        Runnable r = () -> System.out.println(count);
                                              ^
エラー1個

英語では local variables referenced from a lambda expression must be final or effectively final です。エラーの位置はラムダの中(4行目)ですが、原因は5行目の count++ です。ラムダより後ろで再代入していても対象になります。

匿名クラスでも同じルールで、文面の「ラムダ式」が「内部クラス」に変わります。

E8.java:5: エラー: 内部クラスから参照されるローカル変数は、finalまたは事実上のfinalである必要があります
            public void run() { System.out.println(count); }
                                                   ^

通常の for 文のカウンタ i も、i++ で再代入されるためラムダの中では使えません(同じエラーになることを確認済みです)。

なぜこのルールがあるのか

ラムダ式は、外側のローカル変数の値をコピーして持ち運びます。ラムダは作られた場所とは別のタイミング・別のスレッドで動くことがあり、そのときには元の変数(メソッドのスタック上にある)はもう存在しないかもしれません。もし再代入を許すと「コピーと元のどちらが正しい値か」が食い違うので、Java は「変わらない変数だけコピーできる」というルールにしています。

直し方3通り

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class Effective {
    public static void main(String[] args) {
        List<String> names = List.of("A", "B", "C");

        // 1. 一度も再代入しない変数は、final と書かなくてもラムダで使える
        String prefix = "ID-";
        names.forEach(n -> System.out.println(prefix + n));

        // 2. 数えたいときは別の変数にコピーするか、入れ物を使う
        int total = 0;
        for (String n : names) { total += n.length(); }
        final int fixedTotal = total;           // コピーして固定
        Runnable r = () -> System.out.println("合計=" + fixedTotal);
        r.run();

        AtomicInteger counter = new AtomicInteger();   // 参照は固定、中身を更新
        names.forEach(n -> counter.incrementAndGet());
        System.out.println("件数=" + counter.get());

        // 3. 拡張for文の変数は毎回新しい変数なのでラムダで使える
        for (String n : names) {
            Runnable p = () -> System.out.print(n + " ");
            p.run();
        }
        System.out.println();
    }
}

実行結果(java Effective.java):

ID-A
ID-B
ID-C
合計=3
件数=3
A B C 
状況 直し方
計算し終わった値をラムダで使いたい final int fixed = total; のように別の変数へコピーする
ラムダの中で数を数えたい AtomicInteger などの入れ物を使う。集計なら Stream の count()/sum() がより簡潔
ループのカウンタを使いたい 拡張 for 文にするか、ループ内で int n = i; とコピーする
フィールドを使いたい フィールドはこのルールの対象外で、そのまま読み書きできる

2つ目の AtomicInteger は、ここまでに見た「参照は固定・中身は可変」の応用です。変数 counter は再代入していないので実質的final で、中の数だけが増えています。

メソッドにfinalを付ける(オーバーライドできなくなる)

メソッドに final を付けると、サブクラスでそのメソッドをオーバーライドできなくなります。

class Base { final void run() {} }
class Sub extends Base { void run() {} }
E3.java:2: エラー: Subのrun()はBaseのrun()をオーバーライドできません
class Sub extends Base { void run() {} }
                              ^
  オーバーライドされたメソッドはfinalです
エラー1個

「処理の骨組みは親クラスで決めて、一部だけサブクラスに任せる」という設計で役立ちます。

public class Tmpl {
    static class Report {
        // 手順の骨組みは固定(オーバーライドさせない)
        public final String build() { return header() + "\n" + body() + "\n-- end --"; }
        protected String header() { return "[Report]"; }
        protected String body() { return "(empty)"; }
    }
    static class Weekly extends Report {
        @Override protected String body() { return "今週の完了タスク: 12件"; }
    }
    public static void main(String[] args) {
        System.out.println(new Weekly().build());
    }
}

実行結果:

[Report]
今週の完了タスク: 12件
-- end --

build() は final なので、サブクラスが出力の順番を変えることはできません。変えてよい部分(body())だけを差し替えています。

メソッドの種類 final を付けるとどうなるか
インスタンスメソッド サブクラスでオーバーライドできない
static メソッド サブクラスで同名の static メソッドを定義(隠蔽)できない
private メソッド もともとオーバーライドできないので、付けても動作は同じ
abstract メソッド 併用できない(オーバーライドが前提のため)
コンストラクタ 付けられない(継承されないため)

クラスにfinalを付ける(継承できなくなる)

クラスに final を付けると、そのクラスを extends できなくなります。

final class Leaf {}
class More extends Leaf {}
E4.java:2: エラー: final Leafからは継承できません
class More extends Leaf {}
                   ^
エラー1個

標準ライブラリでは String、Integer などのラッパークラス、LocalDate などが final クラスです。「この型の値は、必ずこのクラスが決めたとおりに振る舞う」と保証できるので、値を表すクラスや、変更不可(イミュータブル)なクラスでよく使われます。

変更不可なクラスを自分で作るときの基本形は次の4点です。

ポイント 理由
クラスを final にする(または record にする) サブクラスで振る舞いを変えられないようにする
フィールドをすべて private final にする 作った後に値が変わらないようにする
セッターを作らない 変更の入口を用意しない
List などを受け取るときは List.copyOf でコピーする 渡した側が後から中身を変えても影響を受けないようにする

enum・record・sealedとfinalの関係

Java 16 以降の record、Java 17 以降の sealed は、final と深く関わっています。リフレクションで修飾子を調べた結果が次のとおりです。

import java.lang.reflect.Modifier;
import java.util.List;

public class Kinds {
    enum Status { OPEN, DONE }
    record Item(String name, int qty) {}
    sealed interface Shape permits Circle, Square, Free {}
    record Circle(double r) implements Shape {}
    static final class Square implements Shape {}
    static non-sealed class Free implements Shape {}
    static class Wide extends Free {}
    interface Conf { int MAX = 5; }

    public static void main(String[] args) throws Exception {
        System.out.println("enum   final? " + Modifier.isFinal(Status.class.getModifiers()));
        System.out.println("record final? " + Modifier.isFinal(Item.class.getModifiers()));
        System.out.println("record field  " + Modifier.toString(Item.class.getDeclaredField("name").getModifiers()));
        System.out.println("interface定数 " + Modifier.toString(Conf.class.getField("MAX").getModifiers()));
        System.out.println("String final? " + Modifier.isFinal(String.class.getModifiers()));
        System.out.println("Square final? " + Modifier.isFinal(Square.class.getModifiers())
                + " / Free final? " + Modifier.isFinal(Free.class.getModifiers()));

        // record のフィールドは final でも、中の List は別の話
        record Box(List<String> items) {}
        var src = new java.util.ArrayList<>(List.of("a"));
        Box box = new Box(src);
        src.add("b");
        System.out.println("そのまま渡したrecord: " + box);

        record SafeBox(List<String> items) {
            SafeBox { items = List.copyOf(items); }   // コンパクトコンストラクタでコピー
        }
        var src2 = new java.util.ArrayList<>(List.of("a"));
        SafeBox safe = new SafeBox(src2);
        src2.add("b");
        System.out.println("コピーしたrecord:   " + safe);
    }
}

実行結果(java Kinds.java、Java 21.0.10):

enum   final? true
record final? true
record field  private final
interface定数 public static final
String final? true
Square final? true / Free final? false
そのまま渡したrecord: Box[items=[a, b]]
コピーしたrecord:   SafeBox[items=[a]]
種類 final との関係 反したときのエラー(javac 21.0.10)
enum 定数ごとの本体を持たない enum は暗黙に final。継承できない final Colorからは継承できません/列挙クラスは拡張可能ではありません
record クラスは暗黙に final。フィールドは private final final Pointからは継承できません/final変数xに値を割り当てることはできません
sealed 継承できるクラスを permits で限定。許可されたクラスは final・sealed・non-sealed のどれかを必ず書く sealed、non-sealedまたはfinal修飾子が必要です
interface のフィールド 暗黙に public static final static final変数MAXに値を割り当てることはできません
List.of など final とは別の仕組みで、中身を変更不可にする 実行時に UnsupportedOperationException

ここでも「参照は固定・中身は可変」が顔を出します。record のフィールドは final ですが、List を持たせると、渡した側が後から中身を変えられます(実行結果の Box[items=[a, b]])。コンパクトコンストラクタで List.copyOf しておくと、作った時点の内容で固定できます(SafeBox[items=[a]])。

sealed は「継承を完全に止める final」と「誰でも継承できる通常のクラス」の中間にあたります。

継承の開き方 書き方 継承できる範囲
だれでも継承できる class A {} 制限なし
決めたクラスだけ継承できる sealed class A permits B, C {} permits に書いたクラスだけ
だれも継承できない final class A {} なし

finally・finalizeとの違い

final・finally・finalize は名前が似ていますが、役割はまったく別です。

名前 種類 役割
final 修飾子 変数の再代入・メソッドのオーバーライド・クラスの継承を止める
finally 例外処理のブロック try の後に、例外の有無にかかわらず必ず動く処理を書く
finalize Object クラスのメソッド オブジェクトが回収される前に呼ばれる仕組み。Java 9 で非推奨、Java 18(JEP 421)で削除予定として扱われるようになった
public class Fin {
    static int calc() {
        try {
            return 1;
        } finally {
            System.out.println("finally は return の前に必ず動く");
        }
    }
    @Override
    protected void finalize() { System.out.println("finalize"); }

    public static void main(String[] args) {
        System.out.println(calc());
    }
}

コンパイルと実行の結果(Java 21.0.10、javac -Xlint:removal):

Fin.java:10: 警告: [removal] Objectのfinalize()は推奨されておらず、削除用にマークされています
    protected void finalize() { System.out.println("finalize"); }
                   ^
警告1個
finally は return の前に必ず動く
1

finalize をオーバーライドすると警告が出て、実行しても「finalize」は表示されませんでした(呼ばれるタイミングは保証されていません)。後始末には try-with-resources(AutoCloseable)を使うのが現在の書き方です。

finalを付けるかどうかの判断表

対象 付けるとよい場面 付けなくてよい場面
フィールド 作った後に変わらない値(ID、名前、依存するオブジェクト)。基本は final から考える 状態として変化する値(件数、現在のステータス)
static フィールド 定数(static final) — (変化する static フィールドは共有状態になるため、慎重に設計する)
ローカル変数 チームの規約で決めている場合、長いメソッドで値が変わらないことを示したい場合 短いメソッド(実質的final で十分読み取れる)
引数 チームの規約で決めている場合 規約がなければ省略されることが多い
メソッド 処理の骨組みを固定したいとき、コンストラクタから呼ぶメソッド サブクラスで差し替える前提のメソッド
クラス 値を表すクラス、変更不可なクラス、継承を想定していないクラス 継承して拡張する前提のクラス、モックで差し替えるクラス

「final を付けると速くなりますか」という質問もよくあります。現在の JVM は実行時に最適化を行うので、ローカル変数やメソッドに final を付けるだけで体感できる速度差が出ることはほぼありません。final は速度のためではなく、「変わらない」という意図を伝え、書き間違いをコンパイル時に見つけるために使う、と考えるとよいでしょう。

つまずきやすい点のチェックリスト

症状 原因 直し方
final なのに List の中身が変わっている final は参照だけを固定する List.of/List.copyOf で中身を固定する
定数を変えたのに前の値で動く コンパイル時定数が使う側に埋め込まれている 使う側も再コンパイル(クリーンビルド)する
「初期化されていない可能性があります」 blank final に代入しない経路がある すべてのコンストラクタ・分岐で1回代入する
「すでに代入されている可能性があります」 blank final に2回代入する経路がある 一時変数で計算してから、最後に1回だけ代入する
ラムダで「事実上のfinalである必要があります」 ラムダの前後どこかで変数に再代入している 別の変数にコピーする、AtomicInteger を使う
「定数式が必要です」 case ラベルに Integer などコンパイル時定数でない値を使っている 基本型か String の定数にする、または enum を使う
配列の定数が書き換えられる final 配列の要素は変更できる List.of に置き換える

よくある質問

Q. final と static final はどう違いますか? A. final だけのフィールドはオブジェクトごとに1つずつあり、コンストラクタで別々の値を入れられます(例:会員ごとの ID)。static final はクラスに1つだけで、全体で共有する定数です(例:最大件数)。「オブジェクトごとに違う値か、全体で同じ値か」で選びます。

Q. final を付けたフィールドは、リフレクションでも書き換えられませんか? A. Java 21.0.10 と 25.0.4.1 では、インスタンスの final フィールドは setAccessible(true) の後に Field.setInt で書き換えられることを確認しました(公式の API ドキュメントによると、record のフィールドと static final フィールドはこの方法では書き換えられません)。JDK 26 では JEP 500 により、この操作に警告が出るようになり、将来は既定で禁止される方向と公式に案内されています(JDK 26 での動作は手元では未確認で、公式の JEP で確認した内容です)。通常のコードでは使わない前提で考えるのがよいでしょう。

Q. ローカル変数にも全部 final を付けたほうがよいですか? A. どちらの流儀もあります。Java 8 以降は実質的final があるので、付けなくてもラムダで使えます。大切なのはチーム内でそろえることです。フィールドについては、変わらないものは final にする、という方針が広く使われています。

Q. Java の定数に const は使えますか? A. const は Java の予約語ですが、機能は割り当てられていません。定数は static final で書きます。種類を表す定数は enum を使うと、型で区別できて読みやすくなります。

まとめ

  • final は付ける場所で意味が変わる:変数は再代入、メソッドはオーバーライド、クラスは継承を止める
  • final が固定するのは参照:final な List にも add できる。中身まで固定するなら List.of/List.copyOf
  • 定数は public static final + 大文字スネークケース:複数の値は配列ではなく List.of で持つ
  • コンパイル時定数は使う側に埋め込まれる:値を変えたら使う側も再コンパイルする
  • blank final はすべての経路でちょうど1回代入:0回なら「初期化されていない」、2回なら「すでに代入されている」
  • ラムダで使うローカル変数は実質的final:再代入が必要ならコピーするか入れ物(AtomicInteger など)を使う
  • enum・record は暗黙に final、sealed は継承先を限定:finally・finalize は名前が似ているだけの別物

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java finalの意味を変数・メソッド・クラス別に解説|定数の書き方」

株式会社RJCでは、Javaのfinalのような言語の基本を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?