🍇

8.2 finalと定数~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様に強くお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

8.2 finalと定数

チャプターの概要

このチャプターでは、final修飾子の意味と、定数という「値の再代入が禁止された変数」の用法について学びます。

8.2.1 final修飾子による再代入の禁止

final修飾子とは

final修飾子は、クラス、メソッド、フィールド、ローカル変数に対して付与することができますが、それぞれで意味が異なります。

  • クラスに付与 … 継承を禁止することを表す
  • メソッドに付与 … オーバーライドを禁止することを表す
  • フィールドに付与 … 再代入を禁止することを表す
  • ローカル変数に付与 … 再代入を禁止することを表す

クラスとメソッドに付与する場合については、チャプター11.1で取り上げます。ここではフィールドとローカル変数に付与する場合について、説明します。

フィールドに付与する場合

final修飾子をフィールドに付与すると、フィールド値の再代入を禁止することを明示的に表すことができます。厳密には「必ずフィールド宣言またはコンストラクタによって、一度だけ初期値を代入すること」というルールを課せられる形になります。
例えばPerson2クラスの2つのフィールド、nameとageに、以下のようにfinal修飾子を付与するものとします。

Person2
class Person2 {
    final String name;
    final int age;
    ........
}

このクラスには、以下のように、finalが付与されたnameとageを初期化するためのコンストラクタを必ず実装しなければなりません。

snippet (Person2)
Person2(String name, int age) {
    this.name = name;
    this.age = age;
}

またコンストラクタをオーバーロードして以下のようなコンストラクタを実装すると、今度はコンパイルエラーになります。

snippet
Person2(String name) {
    this.name = name;
}

コンパイルエラーになるのは、このコンストラクタによってインスタンスを生成しても、ageフィールドが初期化されないためです。

ローカル変数に付与する場合

final修飾子をローカル変数に付与する場合も、フィールドと同様に再代入を禁止することを明示します。
例えば、以下のコードのようにローカル変数を初期化した場合、

snippet
final int x = 10;

変数xに対してx = 20といった具合に値を再代入しようとすると、コンパイルエラーになります。
通常のJavaアプリケーション開発では、ローカル変数への再代入を明示的に禁止するケースは一般的にはあまり多くはないでしょう。

8.2.1 定数

定数とは

定数とは広い意味での変数の一種ですが、「一度値が代入されたら変更されない変数」を表します。定数は通常、static修飾子、final修飾子、アクセス修飾子の3つを同時にフィールドに付与して宣言します。また(finalなので必然的に)宣言と同時に初期値も代入します。
定数の構文は、以下のとおりです。

【構文】定数の宣言
public static final データ型 変数名 =; // すべてのクラス間で共有
または
private static final データ型 変数名 =; // 自クラス内でのみ使用

具体的には以下のように宣言します。

snippet
public static final int GENERAL_CUSTOMER = 0;

まず定数は「一度値が代入されたら変更されない」ので、final修飾子によってそれを明示します。また定数は変更される可能性がなく、インスタンス単位で保持する必要はないため、staticキーワードを付与してスタティックフィールドにします。publicやprivateは、アクセス修飾子の一種です。アクセス修飾子についてはチャプター10.1で後述しますが、ここでは「すべてのクラス間で共有される定数」にはpublicを付与し、「自クラス内でのみ使用する定数」にはprivateを付与するものと理解してください。
いずれにしても定数にはそれ専用の特別なキーワードがあるわけではなく、定数の要件から必然的にこのような記述になる、というわけです。

定数の命名

定数は変数の一種のため「レッスン2.1.5 識別子とキーワード」で触れたルールに則ってさえいれば、任意の名前を付けることができます。ただし可読性確保の観点から、定数の命名には英語大文字を用い、単語の切れ目にはアンダースコアを用いる記法を採用するのが一般的なコーディング上の規約です。例えば「ページ毎の最大行数」を表す定数であれば、「MAX_LINE_PER_PAGE」といった名前を付けることになるでしょう。

定数の目的(1):可読性の向上

定数を使用する目的には、可読性の向上、保守性の向上、安全性の確保、といったものがあります。
定数を使用することによって、コードの可読性が向上します。
例としてECサイトにおける顧客種別を取り上げます。顧客が一般会員、ゴールド会員、ダイヤモンド会員という3つの種別に分かれており、それぞれの種別に対してint型の0、1、2というコード値が設定されているものとします。このとき顧客種別を表す変数customerTypeに対して、それがダイヤモンド会員かどうかを判定するためのif文は以下のようになるでしょう。

snippet
if (customerType == 2) { .... } // 2はダイヤモンド会員のコード値

ただしこのコードを見ても、2がダイヤモンド会員を表すコード値であることを直感的に理解することはできません。そこで、Customerクラスの中に顧客種別を定数として宣言します。

Customer
class Customer {
    public static final int GENERAL_CUSTOMER = 0;
    public static final int GOLD_CUSTOMER = 1;
    public static final int DIAMOND_CUSTOMER = 2;
    // その他のフィールド(省略)
    // コンストラクタ(省略)
    // メソッド(省略)
}

この定数を用いると、先ほどのダイヤモンド会員かどうかを判定するためのif文は以下のようになります。

snippet
if (customerType == Customer.DIAMOND_CUSTOMER) { .... }

このコードであれば、ダイヤモンド会員かどうかを判定する処理であることを直感的に理解することができます。

今度は別の例として「配列から要素を取り出して順番にコンソールに表示する。そのとき30行ごとに"改ページ"と表示する」という処理を考えます。以下のコードを見てください。

snippet_1 (Main_NewPage)
for (int i = 0; i < array.length; i++) {
    System.out.println(array[i]);
    if ((i + 1) % 30 == 0)
        System.out.println("改ページ");
}

このコードにおける30は「ページ毎の最大行数が30行である」という業務仕様を表す数字です。このように、コードに埋め込まれた、一見しただけでは意味が理解できない数字を「マジックナンバー」と呼びます。
マジックナンバーはコードの可読性を損なう可能性があるため、基本的には定数に置き換えた方が望ましいでしょう。定数に意味のある名前を付けて宣言し、マジックナンバーである30を置き換えることで、コードの可読性を高めることができます。

snippet_2 (Main_NewPage)
private static final int MAX_LINE_PER_PAGE = 30;

定数の目的(2):保守性の向上

定数の重要な目的の1つに、保守性の向上があります。
コード上の様々な場所に散在してしているコード値や文字列を定数として定義することで、保守性の向上に繋がります。
ここで例として、先ほどのECサイトにおける顧客種別を取り上げます。新たに一般会員とダイアモンド会員の中間的な位置付けとしてプラチナ会員が追加され、コード値2が割り当てられたとします。そして、既存のダイヤモンド会員のコード値はそれに伴って3に変更になりました。
このとき、Customerクラスを以下のように修正します。

snippet
class Customer {
    public static final int GENERAL_CUSTOMER = 0;
    public static final int GOLD_CUSTOMER = 1;
    public static final int PLATINUM_CUSTOMER = 2; // 追加
    public static final int DIAMOND_CUSTOMER = 3; // 変更
    ........
}

新たに定数としてPLATINUM_CUSTOMERを宣言し、コード値2を割り当てます。またそれに伴って定数DIAMOND_CUSTOMERのコード値を3に変更しています。もし定数を使っていなかったら、ダイヤモンド会員かどうかを判定するすべての処理を洗い出し、比較対象を2から3に修正する必要があります。定数を使っている場合はそのような修正は必要ないため、アプリケーションの保守性が高まります。

定数の目的(3):安全性の確保

定数のその他の目的に、安全性の確保があります。
何度も登場する文字列を定数にすることにより、タイプミスを軽減することが可能です。例えばWebブラウザから送信されるUser-Agentヘッダが、"Mozilla/5.0 (Linux; Android 10; K)"かどうかを判定する処理は、以下のようなコードになります。

snippet
if (userAgent.equals("Mozilla/5.0 (Linux; Android 10; K)")) { .... }

もしアプリケーションのいたるところにこの長い文字列が散在する場合、どこかでタイプミスをしてもコンパイラでは検知できません。そこで以下のようにこの文字列を定数にします。

snippet
private static final String MOZILLA_5_0 = "Mozilla/5.0 (Linux; Android 10; K)";

このようにするとタイプミスの発生を軽減することができ、安全性を高めることが可能になります。

定数と列挙型

これまで説明してきたように、定数には様々なメリットがありますが限界もあります。
再びECサイトの顧客種別の例で考えてみましょう。例えば顧客種別がコード値として0から3までの範囲を取り、それが定数として定義されているものとします。このときもし何らかのメソッドで、引数として顧客種別を表すint型変数を受け取るような場合、コンパイラでは値の有効性まではチェックできません(存在しないコード値"4"を渡すことができてしまう)。
このような観点から、昨今では何らかの「静的な値」の組み合わせを実装する場合は、定数よりも列挙型を利用するケースが一般的です。列挙型の詳細については、チャプター15.1で取り上げます。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. final修飾子は、クラス、メソッド、フィールド、ローカル変数に対して付与することができ、それぞれで意味が異なること。
  2. フィールドにfinal修飾子を付与すると、フィールド値の再代入をコンパイラによって禁止できること。
  3. ローカル変数にfinal修飾子を付与すると、値の再代入をコンパイラによって禁止できること。
  4. 定数とは「一度値が代入されたら変更されない変数」であり、staticおよびfinalによって修飾すること。
  5. 定数の命名規約について。
  6. 定数には、可読性の向上、保守性の向上、安全性の確保といった目的があること。
  7. 「静的な値」の組み合わせを実装する場合は、定数よりも列挙型を利用するケースの方が一般的であること。

Discussion