1. 概要

Java言語仕様では、コンパイル時定数という用語は定義されておらず、使用されていません。 ただし、開発者はこの用語をコンパイル後に変更されない値を説明するために使用することがよくあります

このチュートリアルでは、クラス定数とコンパイル時定数の違いについて説明します。 定数式を見て、コンパイル時定数の定義に使用できるデータ型と演算子を確認します。 最後に、コンパイル時定数が一般的に使用されるいくつかの例を見ていきます。

2. クラス定数

Javaで定数という用語を使用する場合、ほとんどの場合、静的および最終クラス変数を指します。 コンパイル後にクラス定数の値を変更することはできません。 したがって、プリミティブ型または文字列のすべてのクラス定数もコンパイル時定数です。

public static final int MAXIMUM_NUMBER_OF_USERS = 10;
public static final String DEFAULT_USERNAME = "unknown";

staticではない定数を作成することが可能です。 ただし、Javaは、クラスのすべてのオブジェクトでその定数にメモリを割り当てます。 したがって、定数の値が実際に1つしかない場合は、staticと宣言する必要があります。

Oracleは、クラス定数の命名規則を定義しています。 アンダースコアで区切られた単語で大文字に名前を付けます。 ただし、すべてのstaticおよびfinal変数が定数であるとは限りません。 オブジェクトの状態が変化する可能性がある場合、それは定数ではありません。

public static final Logger log = LoggerFactory.getLogger(ClassConstants.class);
public static final List<String> contributorGroups = Arrays.asList("contributor", "author");

これらは一定の参照ですが、可変オブジェクトを参照しています。

3. 定数式

Javaコンパイラは、コードのコンパイル中に定数変数と特定の演算子を含む式を計算することができます

public static final int MAXIMUM_NUMBER_OF_GUESTS = MAXIMUM_NUMBER_OF_USERS * 10;
public String errorMessage = ClassConstants.DEFAULT_USERNAME + " not allowed here.";

このような式は、コンパイラがそれらを計算して単一のコンパイル時定数を生成するため、定数式と呼ばれます。 Java言語仕様で定義されているように、定数式には次の演算子と式を使用できます。

  • 単項演算子:+、-、〜、!
  • 乗法演算子:*、/、%
  • 加算演算子:+、–
  • シフト演算子:<<、>>、>>>
  • 関係演算子:<、<=、>、> =
  • 等式演算子:==、!=
  • ビットごとの論理演算子:&、^、|
  • 条件付き-andおよび条件付き-or演算子:&&、||
  • 三元条件演算子:?:
  • 含まれる式が定数式である括弧で囲まれた式
  • 定数変数を参照する単純な名前

4. コンパイル対。 実行時定数

変数の値がコンパイル時に計算される場合、変数はコンパイル時定数です。 一方、実行時定数値は実行時に計算されます。

4.1. コンパイル時定数

Java変数は、がプリミティブ型または文字列であり、finalで宣言され、宣言内で初期化され、定数式を使用している場合、コンパイル時の定数です。

Strings は不変であり、 String pool に存在するため、プリミティブ型に加えて特殊なケースです。 したがって、アプリケーションで実行されているすべてのクラスは、String値を共有できます。

コンパイル時定数という用語には、クラス定数だけでなく、定数式を使用して定義されたインスタンス変数とローカル変数も含まれます。

public final int maximumLoginAttempts = 5;

public static void main(String[] args) {
    PrintWriter printWriter = System.console().writer();
    printWriter.println(ClassConstants.DEFAULT_USERNAME);

    CompileTimeVariables instance = new CompileTimeVariables();
    printWriter.println(instance.maximumLoginAttempts);

    final String username = "baeldung" + "-" + "user";
    printWriter.println(username);
}

最初に出力される変数のみがクラス定数です。 ただし、出力される3つの変数はすべて、コンパイル時の定数です。

4.2. 実行時定数

プログラムの実行中は、ランタイム定数値を変更できません。 ただし、アプリケーションを実行するたびに、異なる値を持つ可能性があります。

public static void main(String[] args) {
    Console console = System.console();

    final String input = console.readLine();
    console.writer().println(input);

    final double random = Math.random();
    console.writer().println("Number: " + random);
}

この例では、ユーザー定義値とランダムに生成された値の2つの時定数が出力されます。

5. 静的コードの最適化

Javaコンパイラは、コンパイルプロセス中にすべてのコンパイル時定数を静的に最適化します。 したがって、コンパイラは、すべてのコンパイル時定数参照を実際の値に置き換えます。 コンパイラーは、コンパイル時定数が使用されるすべてのクラスに対してこの最適化を実行します。

別のクラスの定数が参照されている例を見てみましょう。

PrintWriter printWriter = System.console().writer();
printWriter.write(ClassConstants.DEFAULT_USERNAME);

次に、クラスをコンパイルし、上記の2行のコードに対して生成されたバイトコードを確認します。

LINENUMBER 11 L1
ALOAD 1
LDC "unknown"
INVOKEVIRTUAL java/io/PrintWriter.write (Ljava/lang/String;)V

コンパイラが変数参照を実際の値に置き換えたことに注意してください。 したがって、コンパイル時定数を変更するには、それを使用しているすべてのクラスを再コンパイルする必要があります。 それ以外の場合は、古い値が引き続き使用されます。

6. ユースケース

Javaのコンパイル時定数の2つの一般的な使用例を見てみましょう。

6.1. Switchステートメント

switchステートメントのケースを定義するときは、Java言語仕様で定義されているルールに従う必要があります。

  • switchステートメントのcaseラベルには、定数式または列挙型定数のいずれかの値が必要です
  • switchステートメントに関連付けられた2つのcase定数式が同じ値を持つことはできません

この背後にある理由は、コンパイラがswitchステートメントをバイトコード tableswitchまたはlookupswitchにコンパイルするためです。caseステートメントで使用される値は、コンパイル時の定数であり、一意である必要があります。

private static final String VALUE_ONE = "value-one"

public static void main(String[] args) {
    final String valueTwo = "value" + "-" + "two";
    switch (args[0]) {
        case VALUE_ONE:
            break;
        case valueTwo:
            break;
        }
}

switchステートメントで定数値を使用しない場合、コンパイラーはエラーをスローします。 ただし、 finalStringまたはその他のコンパイル時定数を受け入れます。

6.2. 注釈

Javaでの注釈処理は、コンパイル時で行われます。 つまり、アノテーションパラメータは、コンパイル時定数を使用してのみ定義できます。

private final String deprecatedDate = "20-02-14";
private final String deprecatedTime = "22:00";

@Deprecated(since = deprecatedDate + " " + deprecatedTime)
public void deprecatedMethod() {}

この状況ではクラス定数を使用するのが一般的ですが、コンパイラは値を不変の定数として認識するため、この実装を許可します。

7. 結論

この記事では、Javaのコンパイル時定数という用語について説明しました。 という用語には、プリミティブ型または文字列のクラス、インスタンス、およびローカル変数が含まれ、finalとして宣言され、宣言内で初期化され、定数式で定義されていることがわかりました。

例では、コンパイル時と実行時の定数の違いを確認しました。 また、コンパイラがコンパイル時定数を使用して静的コードの最適化を実行することもわかりました。

最後に、switchステートメントとJavaアノテーションでのコンパイル時定数の使用法を調べました。

いつものように、ソースコードはGitHubから入手できます。