美文网首页
深入理解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

    相关文章

      网友评论

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

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