美文网首页
深入理解Java编译期常量

深入理解Java编译期常量

作者: BlueSocks | 来源:发表于2022-11-04 21:40 被阅读0次

什么是编译期常量

我们知道,我们从写java代码开始,到代码执行的时候,中间一共经历四个阶段:

  • 1 新建.java文件 并写代码,这称为编辑期
  • 2 将.java文件编译为.class文件,这称为编译期
  • 3 将.class文件加载到内存 并 生成.class类,这称为加载期
  • 4 通过.class类去创建对象、执行代码,这称为运行期

其中,除了第一个阶段我们能直接干预,剩余三个阶段,都是jvm自己执行的(当然也有黑科技可以人工干预)。

也就是说,第二阶段是 非人工干预的 第一阶段。在这个阶段就能确定的值,我们就称为编译期常量

编译期常量是指: 在编译期就能确定的"常量"。

既然编译期常量在第二阶段的编译期就能确定其值,那么即使后面第三阶段和第四阶段不走,对它也没有影响,而类加载就发生在第三阶段,所以: 编译期常量不会触发类加载

那么,怎么确定一个变量是否是编译期常量呢?

有两种方法:

  • 1 通过查看编译后的.class文件,来看此变量是否被ConstantValue修饰,被修饰的就是编译期常量,否则就不是。

比如,我们写如下代码:

public class Hello {
    public final int a = 10000;
    public static final int b  = 10000;
    public final long c = System.currentTimeMillis();
    public static final long d = System.currentTimeMillis();
}

然后通过javac Hello.java得到Hello.class文件,再使用javap -verbose Hello.class来查看字节码(这里只截取部分):

public final int a;
    descriptor: I
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
    ConstantValue: int 10000  // 有ConstantValue,说明是编译期常量

  public static final int b;
    descriptor: I
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 10000 // 有ConstantValue,说明是编译期常量

  public final long c;
    descriptor: J
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL // 没有ConstantValue,说明不是编译期常量

  public static final long d;
    descriptor: J
    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL // 没有ConstantValue,说明不是编译期常量

有人说了,我用ide写了代码,难不成挨个使用javac去查看是否是编译期常量?肯定不用,我们可以用第二种方法直接判断是否是编译期常量!

  • 2 如果一个变量被final修饰,并且它的值是常量,那么就是编译期常量。

这里有两点,第一就是必须用final修饰,第二是值必须是常量。 比如:

public int a = 10; // 没用final修饰,不是编译期常量
public final int b = System.currentTimeMillis(); // 值不是常量,所以不是编译期常量

这里有个概念问题,就是常量编译期常量是不一样的。用final修饰的肯定是常量,但这是针对运行期的,准确的说是运行期常量,因为他的特点就是: 运行期不可变!

编译期常量除了在运行期不可变,在编译期也是不可变的,因为在编译期就确定了值。

也就是说: 编译期常量一定是运行期常量,而运行期常量不一定是编译期间常量。或者说: 编译期常量 = 运行期常量 + 值是常量。这个很好理解,被final修饰的就是运行期常量,如果值也是常量,那么就是编译期常量。

编译期常量与类加载

现在我们来证明: 编译期常量不会触发类加载

  • 1 从理论上来说,编译期常量的值是在类加载之前确定的,前面的步骤不依赖于后面的步骤,所以不会触发类加载。现在我们用实例证明。

  • 2 从实例证明,我们知道,一个类被加载的时候,会执行它的静态代码块(有疑问的可以回去翻书),那么我们写如下代码:

public static class Hello {
    // a是编译期常量
    public static final long a = 10;

    // 定义静态代码块,来验证是否触发了类加载
    static {
        System.out.println("a is " + a);
    }
}

然后我们来验证:

public static void main(String[] args) {
    // 直接引用即可
    long a = Hello.a;
}

我们运行代码,发现没有打印任何信息,这就证明,根本就没有触发类的初始化。

现在,我们将a的final修饰符去掉,如下:

public static class Hello {
    // a 不再 是编译期常量
    public static long a = 10;

    // 定义静态代码块,来验证是否触发了类加载
    static {
        System.out.println("a is " + a);
    }
}

然后运行代码,如下:

a is 10

可以看到,触发了类加载。我们继续,这次不去掉final,而是将a的值改为时间戳,让他不再是常量,如下:

public static class Hello {
    public static final long a = System.currentTimeMillis();

    static {
        System.out.println("a is " + a);
    }
}

结果如下:

a is 1633683641015

可见,也触发了类加载。

其实,如果一个变量中有类变量赋值语句 或者 static代码块,就会生成一个<clinit>方法,这个方法将会在类加载阶段的 初始化子阶段 执行。

  • 注意这里的类变量,指的是static修饰的变量,非static修饰的变量叫做对象变量。
  • 注意这里的赋值语句,而不是初始化语句,请仔细体会。
public int a = 10; // 这是赋值语句,因为存在二次赋值的情况
public final int a = 10; 这是初始化语句

<clinit>方法是由jvm收集类中所有类变量的"赋值语句"和"static块"得到的

那么,非static的呢?非static的变量是属于对象一级的,也就是说,肯定要先new出来对象,才能使用,而new对象就会触发类加载,所以这个问题是没有任何意义的。

编译期常量的使用

  • 1 APT技术

如果你从事Android开发,并且你使用了Arouter框架,那么你应该知道,Arouter的@route注解,它的path必须是一个编译期常量。

如果你从事Java开发,并且使用了Spring框架,那么你应该知道,Controller的Mapping注解的path,也必须是一个编译期常量。

有疑问的可以试一下,这里不再废话。

那么为什么呢?因为APT技术工作在编译期,所以必须依赖同时期或者更靠前时期的值,而更靠前时期就是编辑期了,所以只能依赖编译期常量。

  • 2 其他运行在编译期的技术

这个比较宽泛,比如插桩,修改字节码等,都是同样的道理。

总结

  • 1 被static修饰的是类一级的,非static修饰的是对象一级的。
  • 2 被final修饰,并且值是常量的,才是编译期常量。
  • 3 类的编译期常量不会触发类加载。
  • 4 对象一级的要先创建对象才能使用,所以肯定会触发类加载(不管是不是编译期常量)。
  • 5 编译期常量不存在赋值语句,只存在初始化语句。

来自:https://juejin.cn/post/7016626301393960974

相关文章

  • String整理

    浅谈StringBuilder - 简书理解Java常量池 - gegewx - 博客园通过反编译深入理解Java...

  • 深入理解Java编译期常量

    什么是编译期常量 我们知道,我们从写java代码开始,到代码执行的时候,中间一共经历四个阶段: 1 新建.java...

  • 常量

    常量  Java中由final修饰的就是常量。如下:  分类:编译期常量和运行时常量 编译期常量:它的值在编译期就...

  • Java:常量池

    一、概念 1. 常量池的概念 基础概念 常量池在 Java 用于保存在编译期已确定的,已编译的 .class 文件...

  • Java常量池

    一、概念 1. 常量池的概念 基础概念 常量池在 Java 用于保存在编译期已确定的,已编译的 class 文件中...

  • java运行原理

    了解Java的工作原理以帮助自己深入理解java 1.java程序运行图示: 跨平台说明:java编译器 (编译)...

  • Java之常量折叠、常量传播和Global Value Numb

    常量折叠 常量折叠是Java在编译期做的一个优化,简单的来说,在编译期就把一些表达式计算好,不需要在运行时进行计算...

  • 32-实战3-JVM优化之JIT优化

    一、堆,是否是对象分配的唯一选择 在《深入理解Java虚拟机中》关于Java堆内存有如下描述:随着JIT编译期的发...

  • 错题集

    引用final static 的常量编译后存储常量而不是引用 编译后: 如果修改 One.java 后编译One....

  • String源码阅读

    String源码阅读 wiki 通过反编译深入理解Java String及intern 成神之路-基础篇 Java...

网友评论

      本文标题:深入理解Java编译期常量

      本文链接:https://www.haomeiwen.com/subject/whmutdtx.html