编译时常量和变量

53

Java语言文档中写道:

如果原始类型或字符串被定义为常量,并且在编译时已知其值,则编译器会在代码中的所有位置将常量名称替换为其值。这称为编译时常量。

我的理解是,如果我们有一段代码:

private final int x = 10;

接着,编译器会将代码中每个x的出现替换成字面值10


但是假设这个常量在运行时被初始化:

private final int x = getX(); // here getX() returns an integer value at run-time.

与编译时常量相比,是否会有任何性能下降(无论多么微不足道)?


另一个问题是下面这行代码:

private int y = 10; // here y is not final

编译器将final static视为编译时常量,而只有final会在运行时初始化为常量,只有static会在运行时初始化。如果没有final,则是一个变量而不是常量。


9
关于第二点,你是错误的!final int a = 1; 中的a是编译时常量。final int b; b = 1; 中的b不是。 - landry
8个回答

65

编译时常量必须:

  • 被声明为final
  • 是原始类型或字符串类型
  • 在声明时初始化
  • 用常量表达式初始化

所以private final int x = getX();不是常量。

对于第二个问题,private int y = 10;不是常量(在这种情况下是非final),因此优化器无法确定值在未来不会更改。因此,它不能像常量值一样进行优化。答案是:不,它与编译时常量的处理方式不同。


4
JLS(Java 语言规范)中定义了常量表达式:http://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.28 - Ciro Santilli OurBigBook.com
3
再举一个例子来消除疑惑:final int a=1; 是编译时常量,但是 final int a; a=2; 不是。 - Deen John
2
@KorayTugay 你只是在玩术语。在Java世界中,我们使用“final”来表示不会改变的事物。而“constant”(或“编译时常量”)则用于表示常量。这对于那些没有Java经验并来自其他语言的人可能很难理解,但是...你必须承认,“final”和“constant”是不同的东西。https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.4 “常量变量是一个原始类型或类型字符串的final变量,它用常量表达式(§15.28)初始化。” - msi
private final int x是一个常量,但它不是编译时常量。 - Koray Tugay
@KorayTugay 在Java中,_compile-time constant_和_constant_是同义词。如果一个final变量不符合被视为_constant_的标准,则称其为final变量。您在JLS中找不到_compile-time constant_这个术语。msi在他上面的评论中提到了这一点,我也在下面的答案中加入了这一点。 - user9514304
显示剩余5条评论

7

JLSfinal变量和常量进行了以下区分:

final变量

一个变量可以被声明为final。一个final变量只能被赋值一次。如果一个final变量被分配了,除非在分配之前它是明确未分配的(§16 (Definite Assignment)),否则会在编译时出错。一旦一个final变量被赋值,它总是包含相同的值。如果一个final变量持有对对象的引用,则对象的状态可能会因对该对象的操作而更改,但变量始终将引用相同的对象。这也适用于数组,因为数组是对象;如果一个final变量持有对数组的引用,则数组的组件可能会因对数组的操作而更改,但变量始终将引用相同的数组。一个空白final是一个声明缺少初始化器的final变量。

常量

常量变量

是一个原始类型或String类型的final变量,它被初始化为一个常量表达式(§15.28)。

从这个定义中,我们可以看出一个常量必须满足以下条件:

  • 声明为final
  • 是原始类型或String类型
  • 在声明中进行初始化(不是空的final
  • 使用常量表达式进行初始化

那编译时常量呢?

JLS中没有包含“编译时常量”这个短语。然而,程序员经常将“编译时常量”和“常量”这两个术语混用。

如果一个final变量不符合被视为常量的上述标准,则严格来说应该称其为final变量。


3
根据JLS的规定,“常量变量”不一定需要是静态的。因此,“常量变量”可以是静态的或非静态的(实例变量)。但是,JLS对于一个变量成为“常量变量”还有其他要求(除了仅仅是final):
- 只能是String或基本类型 - 只能通过内联初始化,因为它是final的,不允许空白final - 必须用“常量表达式”=“编译时常量表达式”进行初始化(参见下面的JLS引用) 4.12.4. final Variables (JLS) 引用如下:
“常量变量”是指使用常量表达式(§15.28)初始化的原始类型或String类型的final变量。 15.28. Constant Expressions 一个“编译时常量表达式”是指一个表示原始类型或字符串值的表达式,不会突然中止,并仅使用以下内容组成: - 原始类型的字面量和字符串类型的字面量(§3.10.1、§3.10.2、§3.10.3、§3.10.4、§3.10.5) - 强制转换为原始类型和字符串类型(§15.16) - 一元运算符+、-、~和!(但不包括++或--)(§15.15.3、§15.15.4、§15.15.5、§15.15.6) - 乘法运算符*、/和%(§15.17) - 加法运算符+和-(§15.18) - 移位运算符<<、>>和>>>(§15.19) - 关系运算符<、<=、>和>=(但不包括instanceof)(§15.20) - 相等运算符==和!=(§15.21) - 按位和逻辑运算符&、^和|(§15.22) - 条件与运算符&&和条件或运算符||(§15.23、§15.24) - 三元条件运算符?:(§15.25) - 括号表达式(§15.8.5),其包含的表达式是常量表达式。 - 简单名称(§6.5.6.1),它们引用常量变量(§4.12.4)。 - 限定名称(§6.5.6.2),形式为TypeName.Identifier,引用常量变量(§4.12.4)。

1

对于private final int x = getX();,在某些机器上可能会有非常小的性能下降,因为这将涉及至少一个方法调用(除了这不是编译时常量的事实),但正如你所说,这将是微不足道的,那么为什么要费心呢?

至于第二个问题:y不是final,因此不是编译时常量,因为它可能在运行时更改。


1

final关键字意味着变量将被初始化一次且仅一次。实际常量需要声明为static。 因此,编译器不会将您的任何示例视为常量。尽管如此,final关键字告诉您(以及编译器)变量只会被初始化一次(在构造函数或字面上)。 如果需要在编译时分配值,则必须使字段为静态。

性能并没有受到太大影响,但请记住,原始类型是不可变的,一旦创建了一个,它将在内存中保留该值,直到垃圾收集器将其删除。 因此,如果您有一个变量y = 1;,然后将其更改为y = 2;,JVM将在内存中具有两个值,但您的变量将“指向”后者。

private int y = 10; // 这里y不是final

编译器是否将其视为编译时常量?

不是。这是一个实例变量,在运行时创建、初始化和使用。


1

请记住,在下面的代码中,x不是编译时常量:

public static void main(String[] args) {
     final int x;
     x= 5;
}

0

private final int x = getX(); 将在对象第一次声明时被调用。性能“下降”将取决于getX(),但这不是创建瓶颈的类型。


0
简单来说,在编译时,编译器会将引用替换为指定的实际值,而不是使用引用参数。
public static void main(String[] args) {
final int x = 5;
}

例如,在编译时,编译器直接使用初始化值5进行编译,而不是使用引用变量“x”;

请查看此解释


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接