1. 概述

Java 语言规范没有定义甚至没有使用术语“编译时常量”。然而,开发人员经常使用这个术语来 描述编译后不改变的值

在本教程中,我们将探讨类常量和编译时常量之间的差异。我们将研究常量表达式,并了解哪些数据类型和运算符可用于定义编译时常量。最后,我们将看一些常用编译时常量的示例。

2. 类常量

当我们在 Java 中使用术语“常量”时,大多数时候我们指的是 静态最终 类变量。编译后我们无法更改类常量的值。因此, 原始类型或 String 的所有类常量也是编译时常量

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

可以创建非 static 的常量。然而,Java 会为类的每个对象中的常量分配内存。因此,如果常量确实只有一个值,则应将其声明为 static

Oracle 为类常量定义了命名约定。我们将它们命名为大写,单词之间用下划线分隔。然而,并非所有 静态最终 变量都是常量。如果一个对象的状态可以改变,那么它就不是一个常量:

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 语言规范中的定义,以下运算符和表达式可用于常量表达式:

  • 一元运算符:+、-、~、!
  • 乘法运算符:*、/、%
  • 加法运算符:+、-
  • 移位运算符:<<、>>、>>>
  • 关系运算符:<、<=、>、>=
  • 相等运算符:==、!=
  • 按位和逻辑运算符:&、^、|
  • 条件与和条件或运算符:&&、||
  • 三元条件运算符: ?:
  • 带括号的表达式,其包含的表达式是常量表达式
  • 引用常量变量的简单名称

4. 编译常量与运行时常量

如果变量的值是在编译时计算的,则该变量是编译时常量。另一方面,运行时常量值将在执行期间计算。

4.1.编译时常量

如果 Java 变量 是基本类型或 String 、声明为 Final 、在其声明中初始化并使用常量表达式,则该变量是 编译时常量。

字符串 是基本类型之上的一种特殊情况,因为它们是不可变的并且存在于 字符串 池中。因此,应用程序中运行的所有类都可以共享 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);
}

只有第一个打印变量是类常量。但是,所有三个打印变量都是编译时常量。

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);
}

在我们的示例中打印了两个运行时常量,一个用户定义的值和一个随机生成的值。

5. 静态代码优化

Java 编译器在编译过程中静态优化所有编译时常量。因此, 编译器将所有编译时常量引用替换为其实际值 。编译器会对使用编译时常量的任何类执行此优化。

让我们看一个引用另一个类的常量的示例:

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

接下来,我们将编译该类并观察上面两行代码生成的字节码:

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

请注意,编译器将变量引用替换为其实际值。因此,为了更改编译时常量,我们需要重新编译所有使用它的类。否则,将继续使用旧值。

6. 使用案例

让我们看一下 Java 中编译时常量的两个常见用例。

6.1. Switch 语句

在定义 switch 语句的 case 时,我们需要遵守 Java 语言规范中定义的规则:

  • switch 语句的 case 标签需要常量表达式或枚举常量的值
  • 与 switch 语句关联的两个 case 常量表达式不能具有相同的值

其背后的原因是编译器将 switch 语句编译为字节码 tableswitchLookupswitch。 它们要求 case 语句中使用的值既是编译时常量又是 unique

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 语句中不使用常量值,编译器将抛出错误。但是,它将接受 最终字符串 或任何其他编译时常量。

6.2.注释

Java 中的注释处理发生在编译时。实际上,这意味着 注释参数只能使用编译时常量来定义

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

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

尽管在这种情况下使用类常量更为常见,但编译器允许这种实现,因为它将值识别为不可变常量。

七、结论

在本文中,我们探讨了 Java 中的术语“编译时常量”。我们看到该术语 包括原始类型或 String 的类、实例和局部变量,声明为 Final ,在其声明中初始化,并使用常量表达式定义

在示例中,我们看到了编译时常量和运行时常量之间的差异。我们还看到编译器使用编译时常量来执行静态代码优化。

最后,我们研究了 switch 语句和 Java 注释中编译时常量的用法。

与往常一样,源代码可以在 GitHub 上获取。