Javaについて徹底解説!

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

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とした変数に再代入をしようとすると、コンパイルエラーになります。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

6.さいごに

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

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

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