Javaのfinal
は、「変更を禁止する」意味を持つキーワードです。具体的に説明すると、以下大きく3つの使い道があります。
- 変数: 値の再代入を禁止する。
- メソッド: サブクラスでのオーバーライドを禁止する。
- クラス: クラスの継承を禁止する。
その役割はいずれも「意図しない再定義や改変から保護すること」に集約されます。可読性や保守性、安全性を高めるために非常に重要な役割を担うキーワードです。
final変数について
final変数の基本
final
変数とは、一度値を代入したら二度と変更できない変数です。Javaでは「定数」に近い挙動を示しますが、完全に同じではありません。C++など他言語のconst
と混同しがちですが、Javaのfinal
は「再代入を禁止する」という意味を主に持ちます。
public class FinalVariableExample { public static void main(String[] args) { final int number = 10; System.out.println(number); // number = 20; // コンパイルエラーとなる } } // numberに再代入しようとするとコンパイルエラーが発生します。
初期化のタイミング
final
変数は、宣言時またはコンストラクタ内、あるいはインスタンス初期化ブロック内で初期化されることが一般的です。一度しか値を代入できないため、そのタイミングを意識してコードを書く必要があります。
class Person { final String name; Person(String name) { this.name = name; // コンストラクタで初期化 } void display() { System.out.println("名前: " + name); } } public class Main { public static void main(String[] args) { Person p = new Person("太郎"); p.display(); // "名前: 太郎" // p.name = "次郎"; // コンパイルエラー } }
finalとイミュータブル
final
というキーワードを付与しても、その変数が参照するオブジェクト自体が不変(イミュータブル)になるわけではありません。final
は「変数への再代入を禁じる」だけであり、参照先のオブジェクトが持つフィールドの値を変更することまでは禁止しません。
class MyData { int value; } public class ReferenceExample { public static void main(String[] args) { final MyData data = new MyData(); data.value = 100; // OK:参照先のフィールド書き換え // data = new MyData(); // コンパイルエラー:再代入禁止 } }
この例では、data.value
には自由にアクセスでき、値を変更できます。しかし、data
そのものを新たなインスタンスで置き換えることはできません。本当の意味でオブジェクトを変更不可にするには、クラスをイミュータブルに設計する必要があります。
静的フィールドとstatic final
クラス全体で共有する定数値を定義するときには、static final
を用います。たとえば、数学的定数の円周率を表すPI
などは、多くのクラスやメソッドで共通して使われるため定数として定義するのが一般的です。
class Constants { static final double PI = 3.14159; } public class Main { public static void main(String[] args) { System.out.println(Constants.PI); // 3.14159 // Constants.PI = 3.14; // コンパイルエラー:再代入不可 } }
ローカル変数へのfinal適用と“effectively final”
Java 8以降では、匿名クラスやラムダ式内から参照される変数が事実上再代入されていない(“effectively final”)場合、final
と同等に扱われます。明示的にfinal
を付けなくても、再代入されていない変数はラムダ式の中で参照可能です。
しかし、コードの可読性や意図を明確にするために、必要に応じてfinal
を付ける場合もあります。
finalメソッドについて
オーバーライドを禁止する目的
クラスを継承した場合、通常は親クラスのメソッドをサブクラスでオーバーライドできます。しかし、メソッドにfinal
が付いているとオーバーライドが禁止されます。
特に「メソッドの実装を絶対に変えたくない」「継承先でも動作を固定したい」といった場合にfinal
を使用します。
class Parent { final void show() { System.out.println("親クラスのメソッド"); } } class Child extends Parent { // void show() { } // コンパイルエラー } public class Main { public static void main(String[] args) { Child c = new Child(); c.show(); // 親クラスのメソッド } }
メソッドの安定性・保守性向上
final
メソッドを宣言することで、サブクラスが意図せず処理を変えてしまうリスクを防げます。APIとして公開されているクラスの中には、動作の一貫性を保つために、重要なメソッドをfinal
とするケースがあります。
用例
finalクラスについて
継承を禁止する目的
final
がクラスに付与されると、そのクラスを継承できなくなります(参考 クラスの継承とは?)。代表例としてはJava標準ライブラリのString
クラスが有名です。
このようにクラス自体を継承不可にするのは、サブクラスによる動作変更を防ぎたい、もしくは継承することにメリットがないと判断されている場合などが挙げられます。
final class Animal { void sound() { System.out.println("動物の音"); } } // class Dog extends Animal {} // コンパイルエラー
使用頻度と注意点
クラスをfinal
にするというのは、拡張性を意図的に削ぐことを意味します。ライブラリ作者がAPIの動作を固定化したい場合などには有効ですが、一般的な業務システム開発においては、クラス設計段階で「将来的に拡張の可能性はないか」を慎重に検討してから決定すべきです。
代表例:Stringクラス
JavaのString
クラスはfinal
で宣言されているため、サブクラスでString
を拡張することはできません。これは、安全性・不変性(イミュータブル)が極めて重要であることに起因します。String
がもし継承可能であれば、サブクラスで内部の文字列を変更される恐れが生じます。セキュリティリスクや言語仕様の整合性を保つために、final
化が必須とされているのです。
finalキーワードの内部的な仕組み
コンパイル時のチェック
Javaコンパイラはfinal
が付いた変数やメソッド、クラスに対して、コンパイル時にチェックを行います。具体的には、変数に対して再代入しようとしていないか、サブクラスでオーバーライドを行おうとしていないか、クラスを継承しようとしていないかを確認し、問題があればコンパイルエラーを出力します。
JITコンパイラと最適化
final
は、JIT(Just-In-Time)コンパイラによる最適化にも貢献すると言われることがあります。
例えば、final
で宣言された定数はコード中でリテラルとしてインライン化される可能性があります。ただし、実際のパフォーマンス向上は状況により異なるため、過度に「最適化のため」としてfinal
を多用するのはおすすめできません。基本的には設計上の意図(変更不可にしたい)を明確化するために使うべきです。
finalとJavaのメモリモデル
finalフィールドの可視性保証
Javaのメモリモデルでは、final
フィールドは以下のような特性を持ちます。
これは「安全な公開(Safe Publication)」とも呼ばれ、マルチスレッド環境での動作保証に関係します。final
フィールドを使うことで、読み手は「コンストラクタ実行後、この値は常に正しい状態で一貫している」と判断できるのです。
スレッドセーフティとの関係
マルチスレッドプログラミングでしばしば問題となるのは、あるスレッドがオブジェクトを構築途中で他のスレッドに見せてしまうケースです。final
フィールドは「コンストラクタで一度値がセットされたら変更がない」という点で、その後の可視性が規定されています。
ただし、final
の利用だけで万能にスレッドセーフになるわけではありません。オブジェクトの状態が変化する場合は、当然ながら適切な同期や排他制御が必要です。