编译期常量,即 compile-time constant。其看似是一个静态,并不一定是由 static 修饰(static 一般只是用于强调只有一份),但强制要求使用 final 进行修饰。编译期常量完整要求是:
可能引起最大疑问的是“什么是常量表达式”这个问题,这个可以参考 Oracle 的官方文档:
https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html (15.28 小节)。
被官方认为是常量表达式的事物众多,这里就列出几个常用的:
下面列出了可能遇到的所有编译期常量:
public String str1 ="helloworld";
static String str2= "helloworld";
public static String str3 = "hello"+"world";
public final int integer1= 1;
static final int integer2= 1;
public static final int integer3 = -1+2;
final char character1= 'a';
补充知识:
==
操作符返回真。在一定条件下,如果返回假,则意味着两个变量指向的值不都是编译期常量池。char[]
数组,由于其是 final 修饰的,不可变,是编译期常量。严格意义上,我们不能说上面代码中的 str1、str2、str3 为编译期常量,因为他们本质上是引用变量,真正的常量为双引号引起来的"helloworld"。一个小问题:在 final 的修饰下已经足够证明一个变量是不可更改的,为何一定要规定在定义时立即进行初始化的变量才属于编译期常量?
这是因为只有在编译期间完全确定的变量才能笃定地写入到 Class 文件中的常量池(本质上是硬盘上的二进制流)。 如果不在定义之后马上就初始化,就不能保证编译期间能够确定,比如如下带有 if-else 判断的二进制流:
public static void main(String[] args) {
final String string_compile_time ="helloworld";
final String string ;
if (args == new String[]{"hello","world"}){
string = "helloworld";
}else{
string = "hellworld";
}
System.out.println(string_compile_time=="helloworld");
System.out.println(string=="helloworld");
}
从控制台输出:
true
false
就可以看出没有在定义时马上初始化的 final 类型字符串不为编译期常量。这是因为编译期间无法确定 string 变量到底取值如何。既然存在这种情况,所以 Java 设计者们就规定了编译期常量强制要求在定义时需要进行首次且唯一的初始化。
实际上,基本类型包装类大多实现了常量池技术,也就是说即使不使用常量表达式在 final 变量在定义的时候初始化,变量是编译期常量。
即 Byte, Short, Integer, Long, Character, Boolean;
这5种包装类默认创建了数值[-128,127]的相应类型的缓存数据,但是超出此范围仍然会去创建新的对象。如以下测试代码:
public class Test {
public static final Integer integer1 = 127;
public static final Integer integer2 = 127;
public static void main(String[] args) {
System.out.println(integer1==integer2);
}
}
//控制台输出
//true
为验证超出一定大小的数值不支持包装类的常量值技术,可以看下面的代码:
public class Test {
public static final Integer integer1 = 128;
public static final Integer integer2 = 128;
public static void main(String[] args) {
System.out.println(integer1==integer2);
}
}
//控制台输出
//false
自动包装有这种现象的原因是以下两个定义是等价的:
public static final Integer integer1 = 128;
public static final Integer integer2 = Integer.valueOf(128);
上述实际上就是编译器替我们完成自动装箱语法糖的内部本质操作。
java.lang.Integer#valueOf(int)
方法的源码为:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
其中:
可见,当待自动装箱的整数如果在[-128,127]内,那么不会 new
一个 Integer 对象返回,而是返回已创建好的 Integer 数组中的一个元素,数组下表被定义为:i + (-IntegerCache.low
。
参考网址:https://coderanch.com/t/454384/java/compile-time-constant
编译期常量存储于字节码文件 .class 文件中的常量池中,而一个变量由:
组成。前三个的引用都一定存储于字节码的字段信息数据区中,而值实际上存储于字节码文件的 Java 常量池中。而变量值只有是编译期常量时才会存储于字节码的常量池中(如果你对这段不太熟悉,可以复习一下 JVM 中关于字节码文件结构的知识)。
一个显而易见的性质是,如果你定义的编译期常量越多,整个class 文件也会越大。
值得一提的是,无论是编译期常量还是其他变量,最终都是要加载到内存中。在 .class 文件中存储的基本类型变量以及 String 类型/包装类数组对象都会被存储于运行时常量池中。使用 ==
操作符来判断值是否相等,本质上是在判断等号两侧是否指向内存中的同一块地址,而不会是在判断是否指向常量池中的同一个值。
注意:
还有一个运行时常量池,这个常量池和编译期常量没有直接关系,在 《Java 中的常量池》一文中会进行整理。