编译期常量
我们知道在Java的类中通过static final 修饰的变量在程序运行过程中是不能够改变的,我们称之为常量。常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,从而也不会导致定义常量的类的初始化。我们看下面这个示例:
package com.geekymv.test.jvm;
public class Test02ConstClass {
public static void main(String[] args) {
System.out.println(Constant.COUNT);
}
}
class Constant {
public static final String COUNT = "hello world";
static {
System.out.println("const class static block");
}
}
运行main方法,输入结果如下:
hello world
Constant 类中的static 代码块并没有执行!Test02ConstClass 类虽然在源码中引用了Constant 类中的常量COUNT,但其实在编译阶段 jvm 通过常量传播优化,已经将此常量的值hello world存储到了调用类(Test02ConstClass)的常量池中了。以后Test02ConstClass 对常量Constant.COUNT 的引用都变成了对自身常量池的引用,也就是说这两个类在编译成Class 之后就不存在任何联系了。我们甚至可以将Constant 类的 Constant.class 文件删除,Test02ConstClass 类依然可以正常执行。
我们可以通过JDK 自带的反编译工具javap,通过它可以查看java编译器为我们生成的字节码。
javap -c com.geekymv.test.jvm.Test02ConstClass
反编译后的结果
Compiled from "Test02ConstClass.java"
public class com.geekymv.test.jvm.Test02ConstClass {
public com.geekymv.test.jvm.Test02ConstClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hello world
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
通过这行代码 3: ldc #4 // String hello world
我们可以看出常量Constant.COUNT 的值已经是确定了就是hello world 。
助记符ldc表示将int,float或者String类型的常量值从常量池中推送至栈顶供程序使用。
运行期常量
当一个常量的值非编译期间可以确定,那么其值就不会被放到调用类的常量池中。这时在程序运行时,会导致主动使用这个常量所在类,显然会导致这个类被初始化。
package com.geekymv.test.jvm;
import java.util.Random;
public class Test03ConstClass {
public static void main(String[] args) {
System.out.println(ConstClass03.COUNT);
}
}
class ConstClass03 {
public static final int COUNT = new Random().nextInt(100);
static {
System.out.println("const class static block");
}
}
运行main方法,输入结果如下:
const class static block
51
运行时常量相对于常量来说,有一个很重要的特征是:动态性。Java语言并不要求常量一定只能在编译期产生,运行期间也可能产生新的常量,这些常量被放在运行时常量池中。