编译期常量与运行时常量

编译期常量与运行时常量

常量大家都不陌生,但如果你脑袋里一听到这个词,就只能想得起来”常量不可修改“的话,那你就得好好往下读一读了。

1 前言

今天写这篇博客不得不感叹一句,知识真的是得来回嚼来回嚼才消化得了。

为什么突然感叹这个呢?

因为我昨天在看Spring Boot,看了对底层有点懵逼,因为Spring没学扎实,回头复习了下Spring,复习Spring又发现对动态代理模式掌握得不好,动态代理看了又去复习反射,反射看了发现自己对JVM又产生了些疑惑,一下又回到了常量这上边。

经过了自顶向下复习,这会又自底向上推回,知识还是学扎实的好,不多说了,哈哈。

2 常量

在Java程序里,常量用关键字static final修饰,常量又分为:

  • 编译期常量
  • 运行时常量

下面我们就分开来看看,举一些好理解的例子,直观的实验。

2.1 编译期常量

下面是一个编译期常量:

static final int A = 1024;

编译时,所有A的引用都将被替换成字面量(即1024),类型必须是基本类型或String。

2.2 运行时常量

下面就是一个运行时常量:

static final int len = "Rhine".length();

运行时才能确定它的值。

2.3 对类的依赖性(了解)

要是你能理解以下内容应该能给你带来一点收获!当然如果你是还没有学习到关于JVM相关知识的同学,暂时不用深究这一点了。

什么叫对类的依赖性?单从字面上理解就是需不需要类,其实也就是与类的创建有没有关系。

那么类的创建和我常量有什么关系吗?或则更具体来说,编译期常量和运行时常量对类的创建有什么不同的影响?


要解答以上的疑问还得先来看类在什么情况下会创建:

要解答以上的疑问还得先来看类在什么情况下会创建:

JVM的虚拟机规范严格规定了有且只有5种情况必须立即对类进行“初始化”,其中第一条就是:

遇到new、getstatic或invokestatic这4条字节码指令时,如果类没有初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:

(1)使用new关键字实例化对象时

(2)读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)时

(3)调用一个类的静态方法时

以上内容摘抄自《深入理解Java虚拟机》7.2节 类加载的时机

对常量池有疑惑的同学可以参考下这篇文章

其中场景(1)和(3)可以不用说了,常量这部分内容正是涉及到了场景(2),我们来做个很简单的程序来看看。

class Test {
	//静态代码块
	static {
		System.out.println("Class Test Was Loaded!");
	}
	
	//编译期常量
	public static final int num = 10;	
	//运行时常量
	public static final int len = "Rhien".length();
}

public class Main {
	public static void main(String[] args) throws Exception {
		System.out.println("num:"+Test.num);
		System.out.println("=== after get num ===");
		System.out.println("len:"+Test.len);
	}
}
/* 打印输出: * num:10 * === after get num === * Class Test Was Loaded! * len:5 */

代码结构很简单,一旦Test类被初始化,那么就会被立即执行。

根据程序运行结果,我们就可以得出结论了:编译期常量不依赖类,不会引起类的初始化;而运行时常量依赖类,会引起类的初始化。

所以我们再重新捋一捋刚才场景(2)那段话,大致可以理解为:“读取或设置一个类的静态字段(编译期常量除外)时”。

3 编译时常量使用的风险

由于编译时,常量会被替换为字面量,这是JVM提高运行效率优化代码的一种方式,但有时候也会带来一定的麻烦。

如果我们项目超大,项目整个编译一次特别耗费时间,那么我们有可能会只编译代码修改的部分。而一旦我们修改了常量A,但又未重新编译所有引用A常量的部分(即.java文件),那么就会导致未重新编译的那部分代码继续使用A的旧值。


下面写个非常简单的实验看看。

定义常量的Book类:

public class Book {
    //编译期常量,书本价格10元
	public static final int price = 10;
}

定义Student类和mian方法:

public class Student {
	
	public int cost = Book.price;
    
	public void printCost() {
		System.out.println("书费"+this.cost+"元");
	}
	
	public static void main(String[] args) {
		Student stu = new Student();
		stu.printCost();
	}
}
/* 打印输出: * 书费10元 */

【Java文件】Book.java、Student.java文件

【字节码文件】Book.class、Student.class文件

运行结果为:书费10元


现在修改Book.java文件中的price=5,并使用javac命令仅仅只重新编译Book.java文件:

javac Book.java

执行java命令,再次运行main方法:

java Student

观察结果:结果与第一次相同,书费10元
第二次运行结果