Java 关键字 final 理解

2,641 阅读6分钟
原文链接: my.oschina.net

基本概念

Java语言里,final关键字有多种用途,其主题都表示“不可变”,但背后的具体内容并不一样。当final关键字用于修饰类时表示该类不允许被继承;当它用于修饰方法时表示该方法在派生类里不允许被覆写(override)。 当final关键字用于修饰变量时表示该变量的值不可变;静态变量、实例成员变量、形式参数和局部变量都可以被final修饰。 修饰变量的final关键字可以为两种:

  1. final修饰基本类型变量:表示该变量的值不能改变,即不能重新赋值,是“编译时常量”(compile-time constant)
  2. final修饰引用类型:表示该变量的引用地址不能改变,而引用地址里的内容可以变,是“运行时不变量”(runtime immutable variable)

编译时常量

概念

在编译阶段已经可以确定其值的变量 举例:

/**
 * @author chenxin
 * @time 2017-05-06-11:25
 */
 public class FinalTest {
   public  final int a = 1;  // 编译时常量

   public static final int b = 2; // 编译时常量

   public static final Integer c = 3; // 非编译时常量

    public static final Type TYPE = Type.D;  // 非编译时常量

    public final int j;  // 非编译时常量

    public static final long k = b - 1; // 编译时常量

    FinalTest(int j){
        this.j = j;
    }

    static{
        System.out.println("触发类初始化");
    }
}

解释

在上述代码中,变量c,TYPE不是基本类型,变量j需要在对象实例化的时候初始化值,为运行时不变量。 实验:

public class FinalMain {
   public static void main(String[] args) {
        System.out.println(FinalTest.b);

        System.out.println(FinalTest.k);

        System.out.println(FinalTest.c);

        System.out.println(FinalTest.TYPE);
    }
}

运行结果:

2
1
触发类初始化
3
D

说明:在访问变量c,TYPE时触发了类的初始化,验证它们不是编译时常量。

问题:为什么触发类的初始化就不是编译时常量呢,那变量a为何又是呢?

  1. 先看变量a,它虽然没有被static修饰,但通过javap -c FinalTest查看字节码,常量1已经赋值给a了。然而,这么写代码是不优雅的,因为a作为成员变量,每次实例化对象都会在常量池分配内存,导致浪费。所以:对于编译时常量,它就需要使用 static 关键字修饰,这样就是类的所有实例共享了。
  2. 为何触发类的初始化就不是编译时常量呢?因为编译时常量存在于常量池,也会直接把值嵌入在字节码中,也就是Java虚拟机会将常量直接保存到类文件中。访问时是不需要进行类的初始化(下文解释)的。
  3. 编译时常量只有可能是基本类型和String类型,而不可能是任何的引用类型,包括枚举,基本类型的包装类型。

官方文档解释

Oracle的官方文档 15.28 Constant Expressions也详细的解释了,什么是 Compile-time-Constant >

  1. 原始类型字面量,或者String字面量
  2. 能转型为原始类型字面量,或String字面量的常量
  3. 一元运算符(+,-,~,!,但不包含++, --) 和1,2组成的表达式
  4. 多元运算符(*,/和%)和1,2组成的表达式
  5. 附加运算符( additive operators) (+ 或 -)与之前几条组成的表达式
  6. 位移运算符(<<,>>, >>>)和之前几条组成的表达式
  7. 关系运算符(<,<=,>,>= ,不包括 instanceof)与之前几条组成的表达式
  8. 关系运算符(==,!=)与之前几条组成的表达式
  9. 位运算符(&, ^, |)与之前几条组成的表达式
  10. 条件与和条件或运算符(&&, ||) 与之前几条组成的表达式
  11. 三元运算符 (?:)和之前几条组成的表达式
  12. 带括号的表达式,括号内也是常量表达式
  13. 引用常量变量的简单变量
  14. 类中的常量变量引用,使用类的全限定名或类名进行引用(String.class)

小测试

public static final int m = new Random().nextInt(); 属于编译时常量吗?

类初始化

类加载机制包括装载,连接,初始化。

  • 装载就是把二进制的class文件读入Java虚拟机中
  • 连接就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时数据区中,连接分为三个子步骤:验证、准备和解析。验证工作是确认class文件符合Java语言的语义规范。在准备阶段,Java虚拟机为类变量分配内存,设置默认初始值。解析过程是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用。
  • 初始化,本阶段负责为类变量赋予正确的初始值。

Java 编译器把所有的类变量初始化语句和类型的静态初始化器通通收集到 <clinit> 方法内,该方法只能被 Jvm 调用,专门承担初始化工作。 初始化阶段就是执行<clinit> 方法的过程

<clinit> 方法生成的条件 类声明了类变量,且有明确使用类变量初始化语句或静态初始化语句初始化,且类变量初始化语句不是编译时常量。 如:仅包含 public static final int b = 2;则不会生成<clinit> 方法

类的初始化时机

类的初始化时机就是在"在首次主动使用时",那么,哪些情形下才符合首次主动使用的要求呢? 首次主动使用的情形:

  • 创建某个类的新实例时--new、反射、克隆或反序列化;
  • 调用某个类的静态方法时;
  • 使用某个类或接口的静态字段或对该字段赋值时(final字段除外);
  • 调用Java的某些反射方法时
  • 初始化某个类的子类时
  • 在虚拟机启动时某个含有main()方法的那个启动类。

为什么使用final

  1. 定义为常量,在多线程环境下进行共享,一定程度上解决某些并发安全问题
  2. JVM对常量有缓存
  3. 方法内部类连本方法的成员变量都不可访问,它只能访问本方法的final型成员,一个典型的示例就是用Spring的 transactionTemplate.execute,如下:
    private void method(){
     final Object b = new Object;
     final Object c = new Object;
     transactionTemplate.execute(new TransactionCallbackWithoutResult() {
             @Override 
             protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
                 a.update(b);
                 a.update(c);
             }
     });
    }
    
    b,c在方法内必须定义为final类型
  4. 对编译时常量的运算JIT会进行常量折叠,如下:
    static int foo2() {
     final int a = 2; // 声明常量a
     final int b = 3; // 声明常量b
     return a + b;    // 常量表达式
    }
    
    实际效果如下:
    static int foo3() {
     return 5;
    }
    

小测试答案:public static final int m = new Random().nextInt(); 不是编译时常量,原因对方法的调用获得的值不属于编译时常量。

参考

xm-king.iteye.com/blog/104997…

www.zhihu.com/question/28… www.ibm.com/developerwo…


本文对你有帮助?欢迎扫码加入后端学习小组微信群: