编译期常量与运行时常量
编译期常量与运行时常量
常量大家都不陌生,但如果你脑袋里一听到这个词,就只能想得起来”常量不可修改“的话,那你就得好好往下读一读了。
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元