Javaについて徹底解説!

Javaのfinalを大解剖 finalの全てがここにある!!

大石 英人

大石 英人

開発エンジニア/Java20年/Java GOLD/リーダー/ボールド歴2年

Javaのキーワードfinalは、変数やクラス・メソッドで「決めたことを変更できなくする」ことをプログラマーが指定するものです。finalの機能自体はこのように簡単に言えてしまうのですが、なぜそれがプログラミング上で必要なのか、意外と知られていないものです。

この記事ではそんなfinalについて、意義・使い方をばっちりお伝えします。finalは変数やクラス、メソッドに指定でき、それぞれ意味が違いますが、もちろんその違いもきちんとお伝えします。

finalを使いこなすことは、良い品質のJavaのプログラムを作るためには欠かせません。せっかくこの記事にたどり着いたのですから、これを機会にfinalをしっかりと理解して、一歩進んだJavaプログラマーになるきっかけを掴みましょう!!


1.finalとは何か

1-1.finalとは決めたものを変更できなくすること

冒頭でも述べたとおり、finalの機能は「決めたことを変更できなくする」ことをプログラマーが指定するものです。finalの意味を辞書で調べると「最終の、最後の、最終的な、決定的な、究極的な、目的を表わす」(weblio英和辞典)で、この中だと「最後の」や「決定的な」がJavaでの使われ方に近いです。

finalは、同じキーワードを変数とクラス・メソッドに指定でき、以下のようにそれぞれ意味が違います。同じキーワードですが文脈により意味が違うので、少しややこしいですね。

  1. 変数へのfinal:変数へ値の再代入をできなくする(=変数へ値を代入できるのが最初の1回のみになる)
  2. クラスへのfinal:クラスを継承できなくする
  3. メソッドへのfinal:メソッドをオーバーライドできなくする

1-2.finalはプログラマーの意思表示

プログラマーが明示的に指定しない限り、変数・クラス・メソッドがfinalになることはありません(一部のケースを除く)。ですから、finalが指定されているということは、プログラマーがそう指定しようと思った、何かしらの意思・意図があるということです。

そして、finalに違反するとコンパイルエラーになることは必ず覚えておきましょう。コンパイルエラーですから、finalを指定したプログラマーの意思は必ず守られるのです。

このようにfinalには大きな強制力があるので、初心者がfinalを使うのは少し怖いと思われたかもしれません。それに、finalを使わない人は全く使いません。ですが、finalを知らない・全く使わないのは非常に損をしています。

なぜfinalを使わないと損なのかは、この後でいくつか紹介します。finalの用途は大変広く、プログラムミスを防いだり、プログラムの修正をしやすくするなどの効果があるのです。


2.変数へのfinalの基本

2-1.変数へのfinalは再代入できなくすること

Javaの変数はいつでも値や参照先を代えられます。そんな変数をfinalとすると、設定した値や参照先を変えること(再代入)ができなくなります。finalとした変数に再代入をしようとすると、コンパイルエラーになります。

int x = 0; // 普通の変数宣言、初期値あり
x = 1;     // 変数の値を変更

final int y = 10; // 変数をfinalで宣言、初期値あり
y = 100;          // finalとした変数は変更できず、コンパイルエラー!!

String s = "A"; // 普通の参照型変数宣言、初期値あり
s = "B";        // 参照型変数の参照先を変更

final String s2 = "C"; // 参照型変数をfinalで宣言、初期値あり
s2 = "D";              // finalとした変数は変更できず、コンパイルエラー!!

2-2.finalとできる変数の範囲

finalとできる変数は、ローカル変数・引数・フィールド(インスタンス変数・クラス変数)の全てです。下記サンプル内のfinalな変数は、いずれもコンパイルエラーとはなりません。変数のfinalは、概ねどこでも使えると考えても良いでしょう。

class FinalSample {
	static final String STATIC_FINAL_VAR = "ABC"; // finalなクラス変数
	final int finalIstanceVar = 1; // finalなインスタンス変数

	void method(final Object finalArgument) { // finalな引数
		final char finalLocalVar = 'X'; // finalなローカル変数

		try (final FileReader finalTry = new FileReader("file")) { // finalなtry-catch内変数
		} catch (final Exception finalEx) { // finalなローカル変数(例外)
		}

		// finalなforループ内ローカル変数
		int[] array = {1, 2, 3};
		for (final a : array) {
		}

		/*
		// forの中でのfinalな変数宣言はできますが、i++はできないのでカウンターとしては使えません
		for (final int i = 0; i <= 10; i++) {
		}
		*/
	}
}

2-3.finalな変数は定数として使われる

finalな変数の最も一般的な使われ方は「定数」です。つまり、プログラム全体で共通的に用いる、未来永劫変わらない値を宣言するのに使います。例えば以下のようなものです。

public class Constant {
	public static final float PI = 3.14F; // 円周率
}

このように変数を宣言しておけば、プログラム全体で Constant.PI という変数で円周率を参照できます。しかも、誰かが勝手に値を変えることを心配せずに使えます。finalでないと、誰かがこっそり Constant.PI = 3.0 とするかもしれませんし、しかもそれに気付けないのです。

また、何かの値を持つ変数を一つのみとすることで、値を変更する際の修正範囲を最小限とできます。上の例で言えば、1行変えるだけで済むからです。個々のプログラムでバラバラの円周率を使っては、例えば計算精度を上げるために 3.141 にしようとした時、大変なことになるのは想像できるでしょうか。


3.クラス・メソッドへのfinalの基本

3-1.クラスへのfinalは継承を打ち切る

クラスをfinalとすることの意味は、そのクラスを継承(extends)させられなくするということです。finalな変数と同様に、このルールに違反しようとするとコンパイルエラーになりますので強制力があります。例としては以下の通りです。

final class FinalSample {
}

class ChildFinalSample extends FinalSample { // コンパイルエラー!! finalなクラスは継承できない
}

クラスを継承させたくないケースは頻繁にあります。Javaの継承は、サブクラスで振る舞いを柔軟に追加・変更できるなどいいことも多いのですが、逆に他のプログラマに余計なことをさせたくないこともあるのです。

3-2.メソッドへのfinalはオーバーライドを打ち切る

クラスへのfinalは継承をできなくしますが、クラスは継承しても良いけれども、インスタンスメソッドのオーバーライドはさせたくない場合は、対象のメソッドをfinalとします。すると、サブクラスでオーバーライドしようとした時にコンパイルエラーになります。

class FinalSample {
	final void method() { // finalなインスタンスメソッド
	}
}

class ChildFinalSample extends FinalSample { // finalなクラスではないので継承はできる
	void method() { // コンパイルエラー!! finalなインスタンスメソッドはオーバーライドできない
	}
}

ただし、finalなインスタンスメソッドであっても、オーバーロードはできます。オーバーライドとは扱いが違いますので、間違えないようにしましょう。

class FinalSample {
	final void method() { // finalなインスタンスメソッド
	}

	void method(int x) { // finalなインスタンスメソッドはオーバーロードできる
	}
}

クラスメソッドは少し事情が違います。クラスメソッドはあくまでクラスの持ち物であり、サブクラスへは継承されず、メソッドのシグネチャが同じでも関係のない別物として扱われます。ですので、以下はJavaでは妥当です。

class FinalSample {
	static final void method() {
	}
}

class ChildFinalSample extends FinalSample {
	static void method() { // methodはFinalSampleではfinalだが、コンパイルエラーとはならない
	}
}

3-3.メソッドやクラスをfinalとしたいケース

変数のfinalと同様に、クラスやメソッドがfinalである場合は、そのようにしたプログラマーの意思を読み取ってみましょう。

良くあるのは、そのメソッドが既に完成しているということを表現したり、扱いが難しいので、サブクラスでもオーバーライドせずそのまま使ってほしいというケースです。

Javaのクラスは原則として誰でも継承でき、メソッドも自由にオーバーライドできます。その際、元々のクラスの制作者が想定したおりに使われる保証はないのです。同じ仕事場の人ならまだしも、オープンソースで全世界に公開され、誰が使うかわからないクラスでは、誤用を防止するための配慮も必要なのです。

そのためにも、できうる限りでクラスやメソッドの意図や使い方を伝える方法の一つとして、finalがあるのです。もちろん、Javadocで明確に伝えるということも大事ですね。


4.【中級者向け】変数へのfinalを使う時に意識すべきこと

4-1.finalとした変数の初期化には注意せよ

4-1-1.ローカル変数のfinal

前述の通り、finalとした変数には1回しか代入ができません。ですが、finalな変数を宣言したタイミングと、代入を行うタイミングはずれていても問題はありません。要は、その変数を使い始める時点で初期化されていればいいのです。

void method() {
	final int a; // 初期化されていないが、コンパイルエラーにはならない
	System.out.println(a); // コンパイルエラー!! finalだが値が設定されていないため使用できない
	a = 10;
	System.out.println(a); // 変数に値が設定されたため参照できる
	a = 123; // コンパイルエラー!! finalなので再代入はできない。
}

4-1-2.インスタンス変数のfinal

finalなフィールド(インスタンス変数・クラス変数)も宣言と同時に初期化をしなくてもよいという意味ではローカル変数と同じですが、初期化が完了していなければならないタイミングがローカル変数とは少し異なります。

finalなインスタンス変数は、インスタンス生成処理(=コンストラクタ、あるいはインスタンスイニシャライザでの全処理)が終わるまで、初期化を遅らせることができます。これは良く使われますので、覚えておきましょう。

class FinalSample {
	final int x; // finalなインスタンス変数その1
	final int y; // finalなインスタンス変数その2
	
	// FinalSampleのコンストラクタ
	FinalSample() {
		System.out.println(x); // コンパイルエラー!! finalだが値が未設定な変数を参照しているため
		x = 100; // finalなインスタンス変数への値の設定
		x = 1000; // コンパイルエラー!! 値が設定されているfinalな変数へ再代入したため
		// コンストラクタが終わるまでにyの初期化がされていなければ、コンパイルエラーとなる
	}
}

4-1-3.クラス変数のfinal

finalなクラス変数は、staticイニシャライザを含むクラス全体の初期化(=ClassLoaderによるクラスのロード)が終わるまで、初期化を遅らせることができます。finalなインスタンス変数の遅延初期化よりは使用頻度は低いですが、それでも頻繁に目にするものです。

class FinalSample {
	static final String X; // finalなクラス変数、未初期化なのでこのままではコンパイルエラー
	static final String Y = "ABC"; // OK
	
	// staticイニシャライザ
	static {
		X = "ABC"; // finalなクラス変数への参照先設定
		X = "DEF"; // コンパイルエラー!! 値が設定されているfinalな変数へ再代入したため
	}
}

4-2.finalな参照型変数は不変であることを意味しない

finalな参照型変数は、参照先のインスタンスが保持する値や状態が変わらないというわけではありません。あくまで変数の参照先のインスタンスを変えられないだけです。これを正確に理解していない人が意外に多いのです。

例えば、以下のようなケースです。finalとしても、参照先のインスタンスの値や状態は簡単に変わるということの意味が分かるかと思います。

final int[] array = new int[] {1, 2, 3}; // 配列をfinalな変数として宣言した
array[0] = 12345; // 変数arrayの参照先は変えられないが、配列の値は変更できてしまう

final List<String> list = new ArrayList<>(); // Listをfinalな変数として宣言した
list.add("ABC"); // 変数listの参照先は変えられないが、メソッドで参照先インスタンスの状態を変えられる
list.add("DEF");

ですから、参照型変数の参照先の不変性(Immutable/イミュータブル)を実現・表現するためにはfinalだけでは不十分です。そして、安易にfinalな参照型変数をpublicにして公開するのは、誰でも状態を変更できるので大変危険でもあるのです。

例えばListMapSetなどのコレクションを使うのであれば、例えばCollections.unmodifiableList()などを使って不変なListとするなどの、ひと工夫が必要となります。

4-3.finalであることの意味を推測しよう

プログラム全体での定数とする以外にもfinalは使われます。その場合はただの変更不可能な変数になりますが、プログラマーがその変数をfinalとした意味をしっかり推測すべきです。

繰り返しですが、finalであるということは、いかなることがあろうともその変数の値や参照先を変えたくないというプログラマーの意思の表れです。用途としては、変数を誤って再代入することにより起こるバグを未然に防ぎたいというケースが最も多いでしょう。

int count = someProcess(); // 処理結果件数を変数に格納、これをreturnするのが仕様
:
count = 123; // 誰かが誤ってcountの内容を更新してしまった!!
:
return count; // 処理結果件数をreturnしたいのだけれど、正しい結果ではない(バグ)

また、変数の値が変わらないことは、プログラムの読みやすさや、プログラムの実行時の最適化のしやすさにも繋がります。ですが、全ての変数をfinalとしては変数の数が増えるので、バランスが大事です。ここでは詳細は述べませんが、興味があればキーワード「プログラミング 副作用」で検索してみましょう。

あとは、フィールドをfinalとすることで、そのクラスのインスタンスが生存している限り、対象のフィールドの状態が変わらないことをコンパイラレベルで保証できます。しかもコンストラクタなどでの初期化が必須なので、初期化が必ず行われることが保証されるのです。このように決めたルールを強制できます。

ただ、finalをどういうケースで使うべきかは議論があります。できる限りfinalを付けるべき、あるいは付けないべきと主張する人がいたり、プロジェクトでのコーディングルールとして指定される場合もあります。その場のルールになるべく従うべきですが、自分としての考えを持つことも大事です。


5.一歩先へ進むためのfinalの知識

以上が普通にJavaでプログラミングをする上で良くあるfinalの使い方ですが、ここではもう少し突っ込んだお話をしてみます。

5-1.抽象クラス・インターフェイスはfinalとはできない

抽象クラスやインターフェイスをfinalにはできません。少し考えると当然なのですが、面と向かってその理由を問われた時に、言葉に詰まることはないでしょうか?

// コンパイルエラー!!
final interface FinalInterface {
}

// コンパイルエラー!!
abstract final class FinalAbstractClass {
}

finalとしてしまうと継承や実装ができなくなってしまうので、コンパイルエラーとする仕様になっているのです。その背景には、抽象クラスやインターフェイスは、サブクラスで継承や実装をすることがその存在意義である…という考え方があるように思います。

抽象的なものだけ決めてあっても、それを実装する具体的なものがなければ、決してプログラムを動かせないということですね。

ちなみに、この仕様はいわゆる定数クラスや定数インターフェイスへの、Javaの仕様策定者からの一つの回答でもあると感じます。定数だけが存在するクラスやインターフェイスは、実務上で必要になることは多いのですが、あくまでJavaenumの仕様がなかった頃のプラクティスであっただけなのです。

5-2.インターフェイスのフィールドは暗黙的にfinalになる

インターフェイスは抽象メソッドやデフォルトメソッドの他に、定数としてのフィールドを持てます。その際、暗黙的にpublic static finalとなることは、案外知られていません。

ですので、例えば以下の記述をした時も、フィールドは必ずfinalになり、後から値の変更はできません。インターフェイスのフィールドは静的かつ変更不能であることがJavaの仕様なので、finalと明示しなくてもfinalになるのです。

interface InterfaceSample {
	int ABC = 100; // これはpublic static final intと宣言しているのと同じである
}

5-3.【中級者向け】実質的なfinal

変数をfinalとしないのにfinalとして扱われる時があります。良くあるのは内部クラスを作る時や、ラムダ式です。特に今利用が広がっているラムダ式では、ラムダ式の外にある変数を参照する際は、その変数はfinalでなければなりません。

final int x = 10;
Supplier s = () -> {return x;}; // xはfinalなので、ラムダ式の中から参照できる

ただ、ラムダ式や内部クラスで参照したい変数を全てfinalとするのはプログラマーの負担になるので、プログラムの文脈上で値や参照先のインスタンスが変わらない変数は、実質的にfinalな変数として扱われます。

例えば、以下のプログラムは妥当ですが、途中でxの値が変更されるとコンパイルエラーになります。

// 実質的finalの例
int x = 10; // finalとはされていない
Supplier s = () -> {return x;}; // xはfinalではないが、実質的にfinalな変数として扱われる

// 実質的finalではない例
int x = 10;
Supplier s = () -> {return x;}; // 後ろでxが再代入されているので、finalではないとされコンパイルエラー
x = 100;

5-4.【上級者向け】finalな変数やメソッドは最適化の対象となる

finalな変数は、値や参照先のインスタンスが変わらないことが保証されています。finalなメソッドは処理内容が変わらないことが保証されています。ですので、様々な最適化においてfinalな変数、特にプリミティブ型でstatic finalな変数やfinalなメソッドは特別扱いされることが多々あります。

プログラムの最適化に興味がある方ならご存知でしょうが、値や処理内容が変わらないということは、プログラム上で決め打ちにできるということです。コンパイラにもよるでしょうが、変数やメソッドがインライン展開されて、値や処理内容が直接埋め込まれることがあります。

例えば、クラスファイルのバイトコードを確認すると、参照先の定数の値が埋め込まれていることが分かると思います。興味があれば見てみると面白いですよ。そして、これがAPI設計者の悩みどころでもあったりするのです(詳細は「APIデザインの極意 Java/NetBeansアーキテクト探究ノート」などで読めます)

public class StaticFinalSample {
	public static final int VALUE = 123;
}

public class StaticFinalRefer {
	private int value = StaticFinalSample.VALUE;
}

StaticFinalReferのバイトコード:
public class StaticFinalRefer {
	  
  // フィールド記述子 #6 I
  private int value;
	  
  // メソッド記述子 #8 ()V
  // スタック: 2, ローカル: 1
  public StaticFinalRefer();
     0  aload_0 [this]
     1  invokespecial java.lang.Object() [10]
     4  aload_0 [this]
     5  bipush 123 ←StaticFinalSample.VALUEの123が埋め込まれている!!
     7  putfield StaticFinalRefer.value : int [12]
    10  return
      行番号:
        [pc: 0, 行: 1]
        [pc: 4, 行: 2]
        [pc: 10, 行: 1]
      ローカル変数テーブル:
        [pc: 0, pc: 11] ローカル: this インデックス: 0 型: StaticFinalRefer
}

5-5.【上級者向け】実はリフレクションでfinalな変数を変更できる

今まで散々finalな変数の値は変えられないと言ってきましたが、実はリフレクションを使うことで、finalなフィールドの値を変更出来たりします。普通の人にとって、実用性は全くありませんし、これを使う必要も全くありませんが。例えば以下の通りです。

import java.lang.reflect.Field;

class RefrectionSample {
	final int finalVar = 123;
	
	public static void main(String[] args) {
		RefrectionSample sample = new RefrectionSample();
		Field f = RefrectionSample.class.getDeclaredField("finalVar");
		f.setAccessible(true);
		f.setInt(sample, 999);
		System.out.println(sample.finalVar); // 123のまま
		System.out.println(f.getInt(sample)); // 実際の値は999になっている
	}
}

6.さいごに

以上お伝えしてきた通り、Javaでのfinalと一言に言っても、いろいろな使い道がありますし、考えるべきこと、注意すべきことが多くあります。

繰り返しですが、finalにはコンパイルエラーを引き起こす強制力がありますので、意味なく、節度なく使うと周囲の混乱を呼びます。普段プログラミングをする上でも「この変数はfinalにするべきだろうか」などと、少し考えてみましょう。

大事なのは、なぜ変数やクラス、メソッドがfinalとなっているのか、そのプログラムを書いた人の気持ちや、プログラムを読む人の気持ちになって考えることです。その様な経験を積むことで初めて、適切なfinalの使い方ができるようになるでしょう。

私たちは、全てのエンジニアに市場価値を高め自身の望む理想のキャリアを歩んでいただきたいと考えています。もし、今あなたが転職を検討しているのであればこちらの記事をご一読ください。理想のキャリアを実現するためのヒントが見つかるはずです。

『技術力』と『人間力』を高め市場価値の高いエンジニアを目指しませんか?

私たちは「技術力」だけでなく「人間力」の向上をもって遙かに高い水準の成果を出し、関わる全ての人々に感動を与え続ける集団でありたいと考えています。

高い水準で仕事を進めていただくためにも、弊社では次のような環境を用意しています。

  • 定年までIT業界で働くためのスキル(技術力、人間力)が身につく支援
  • 「給与が上がらない」を解消する6ヶ月に1度の明確な人事評価制度
  • 平均残業時間17時間!毎週の稼動確認を徹底しているから実現できる働きやすい環境

現在、株式会社ボールドでは「キャリア採用」のエントリーを受付中です。

まずは以下のボタンより弊社の紹介をご覧いただき、あなたの望むキャリアビジョンをエントリーフォームより詳しくお聞かせください。

コメント

公式アカウントLINE限定!ボールドの内定確率が分かる無料診断実施中
公式アカウントLINE限定!
ボールドの内定確率が分かる無料診断実施中