こんにちは、かつコーチです。
「他の人のコードでfinalという単語をよく見るけれど、付けても付けなくても動くから意味が分からない」と感じたことはありませんか。
final(変数の再代入・メソッドのオーバーライド・クラスの継承を禁止するキーワード)は、意図しない変更を防ぐための重要な仕組みです。
この記事では、finalを変数・メソッド・クラスの3パターンで使い分ける方法を解説します。
finalキーワードとは
変更・拡張を禁止する仕組み
finalは、付ける対象によって意味が変わる点が特徴です。
| 対象 | finalの効果 |
|---|---|
| 変数 | 一度代入した値を再代入できなくする |
| メソッド | サブクラスでのオーバーライドを禁止する |
| クラス | そのクラスを継承できなくする |
いずれも共通しているのは、「後から変更・上書きされたくないもの」に対してfinalを付ける、という考え方です。
なぜfinalが必要なのか
finalを使う最大の目的は、意図しない変更によるバグを防ぐことです。
たとえば「消費税率」のような、プログラム実行中に変わってほしくない値にfinalを付けておけば、うっかり再代入するコードを書いた時点でコンパイルエラーとして検知できます。
バグは「実行するまで気づけないもの」が一番厄介です。
finalを使えば、コンパイルの段階で問題を発見できるため、実行時エラーを未然に防げます。
基本の書き方・実装手順
手順1:final変数(再代入の禁止)
public class TaxCalculator {
static final double TAX_RATE = 0.10; // 定数として扱う
public int calculateTax(int price) {
return (int) (price * TAX_RATE);
}
}
finalを付けた変数は、慣習として大文字+アンダースコア区切り(TAX_RATE)で命名することが多く、static finalと組み合わせて定数として扱うのが一般的です。
メソッド内のローカル変数にもfinalを付けられます。
public void printMessage() {
final String greeting = "こんにちは";
// greeting = "さようなら"; ← コンパイルエラー
System.out.println(greeting);
}
手順2:finalメソッド(オーバーライドの禁止)
public class Vehicle {
public final void checkSafety() {
System.out.println("安全確認を実施しました");
}
}
public class Car extends Vehicle {
// public void checkSafety() { ... } ← コンパイルエラー:finalメソッドは上書きできない
}
安全確認のように「サブクラスで勝手に処理を変えてほしくないメソッド」にfinalを付けます。
手順3:finalクラス(継承の禁止)
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
}
// class SpecialPoint extends ImmutablePoint { } ← コンパイルエラー:継承できない
Java標準ライブラリのStringクラスもfinalクラスとして定義されており、Stringを継承した独自クラスは作れません。
つまずきやすい設定・注意点
final変数は「宣言時」または「コンストラクタの中で1回だけ」代入すればよく、必ずしも宣言と同時に初期化する必要はありません。
public class User {
private final String name;
public User(String name) {
this.name = name; // コンストラクタ内での初期化もOK
}
}
ただし、コンストラクタの中で1回代入した後は、それ以降どこからも再代入できません。
よくあるつまずきポイント・エラー対処
final変数への再代入でエラーになる
public class Sample {
public static void main(String[] args) {
final int MAX_SCORE = 100;
MAX_SCORE = 120; // コンパイルエラー
}
}
error: cannot assign a value to final variable MAX_SCORE
私が実際にこのエラーに遭遇したのは、ループの中でfinalな合計値変数を使い回そうとしたときでした。finalは「一度きりの代入」しか許さないため、値を更新するループ変数には使えません。
finalは「変わらない値」専用のキーワードであると覚えておきましょう。
finalな参照型変数でも中身は変更できる
もう1つよくある誤解が、「final変数にすればオブジェクトの中身も一切変更できなくなる」という思い込みです。
❌誤解した書き方
public class Sample {
public static void main(String[] args) {
final java.util.List<String> names = new java.util.ArrayList<>();
names.add("田中"); // これはコンパイルエラーにならず、実際に追加できてしまう
System.out.println(names); // [田中]
}
}
finalが禁止しているのは「変数が指し示すオブジェクトそのものを別のオブジェクトに差し替えること」だけです。
オブジェクトの中身(リストの要素追加など)を変更することは、finalとは無関係に可能です。
✅正しい理解
final java.util.List<String> names = new java.util.ArrayList<>();
names.add("田中"); // OK:中身の変更は可能
// names = new java.util.ArrayList<>(); ← コンパイルエラー:別オブジェクトへの差し替えは不可
「finalを付けたら完全に変更不可になる」ではなく、「変数の参照先を差し替えられなくなる」という理解が正確です。
本当にオブジェクトの中身まで変更不可にしたい場合は、後の記事で紹介する不変オブジェクト設計やrecordの活用が有効です。
応用・一歩先の使い方
定数はstatic finalで一元管理する
税率や上限件数のような定数は、static finalで1箇所にまとめて定義しておくと、後から値を変更する際に修正箇所が1つで済みます。
public class Constants {
public static final double TAX_RATE = 0.10;
public static final int MAX_ITEMS = 100;
}
int tax = (int) (price * Constants.TAX_RATE);
マジックナンバー(コード中に直接書かれた意味不明な数値)を避ける意味でも、定数化は実務でよく使われるプラクティスです。
まとめ
この記事のポイント
- finalは変数・メソッド・クラスに付けられ、それぞれ「再代入禁止」「オーバーライド禁止」「継承禁止」を意味する
- final変数は宣言時かコンストラクタ内で1回だけ代入すればよい
- final参照型変数でも、オブジェクトの中身自体は変更できる点に注意する
- 定数は
static finalで一元管理すると保守性が上がる
次に読むべき記事
- スコープと変数の生存期間については、次の記事で解説します
- 不変オブジェクトの設計については、後半の設計・アーキテクチャ編で改めて詳しく扱います
タグ: Java, 中級者向け, 基本文法