由final修饰局部变量引发的思考

我们都知道局部变量存在栈中,准确的说是存在每个栈帧中的局部变量表中。局部变量的生命周期随方法

的入/出栈开始或者结束,但一旦对局部变量加上了final修饰符,很多人往往会觉得这个变量变得“不一样”了。

  • 误区一:认为final修饰的局部变量会被存储在常量池中

    这是一个很容易让人产生误解的地方,我们都知道常量池中存储的是字面量和符号引用,而final修饰的常量值即是字面量的一种。

    void test(){
        final int num = 5;
        ...
    }
    

     

有些人可能会误以为num这个变量会被存储在常量池,但实际上5这个字面量才会被存储在常量池中。无论它是否被final修饰,5都会被存储到常量池中。上面这段代码可以理解为将5从常量池中取出赋给num变量。

其实final关键字对于变量的存储区域是没有任何影响的。jvm规范中,类的静态变量存储在方法区,实例变量存储在堆区。也就是说static关键字才对变量的存储区域造成影响。final关键字来修饰变量表明了该变量一旦赋值就无法更改,同时编译器必须保证该变量在使用前被初始化赋值。

  • 误区二:认为final修饰的局部变量在方法结束调用后仍然存在(生命周期变长)

    这种误解往往是由于Java中一个经典的例子引发的:使用匿名内部类访问外部变量时,需要使用final修饰。(JDK1.8语法糖,底层会自动加上final,无需开发者再手动添加)

      //初始化按钮的监听器
        public void initListener(Button btn ){
            final int name = "王大锤"; //必须标记为final
    
            btn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    btn.setText(name); //动态改变按钮的文字
                }
            });
        }
    

     

    • 在方法内实现了一个匿名内部类,该类的存活时间大部分情况下肯定比方法要长,如果直接通过局部变量访问,往往会出现问题,因为两者的生命周期不同。
    • Java为了解决这个问题,在底层将局部变量的值传入到了匿名内部类中,并且以匿名内部类的成员变量的形式存在,这个值的传递过程是通过匿名内部类的构造器完成的。简单来说,就是对局部变量拷贝了副本。
    • 如果不用final修饰,则原先的局部变量可以发生变化。这里到了问题的核心了,如果局部变量发生变化后,匿名内部类是不知道的(因为他只是拷贝了局部变量的值,并不是直接使用的局部变量)。这里举个栗子:原先局部变量指向的是对象A,在创建匿名内部类后,匿名内部类中的成员变量也指向A对象。但过了一段时间局部变量的值指向另外一个B对象,但此时匿名内部类中还是指向原先的A对象。那么程序再接着运行下去,可能就会导致程序运行的结果与预期不同。

因此为了保护数据的一致性,Java要求被匿名内部类访问的局部变量必须被final修饰。但不少人仍会觉得是final这个关键字让这个局部变量值被存储到了常量池,譬如这位老哥:

1556806388981

题主也解释的很清楚,final修饰变量的语义只有初始化一次后就不能再被修改值或者引用(对于被引用的对象来说,仍可以被改变)。对于字符串常量值来说本来就存在常量池中,(在JDK1.8以后,常量池被移至堆中,因此也就是存在堆中了)就好比假设在一个方法中:

public void test(){
        String s1 = "abc";
        String s2 = "abc";
}

很明显,上面的s1==s2是成立的,因为他们都指向了常量池中的abc。但是s1和s2是分配在常量池里的吗?当然不是,他们是分配在方法栈的局部变量表中的两个不同变量,只是指向了一个地址而已。

而对与成员变量来说,加不加final修饰都不影响成员变量存储在哪,符合下图的字面量和符号引用都会被存储在常量池中,当然如果将一个对象实例赋给局部变量,虽然这个局部变量的符号引用会存储在常量池,但这个常量池项中肯定有指向堆内对象实例的指针。在这个也会产生不理解的地方:

1556882827649

  • 误区三:为什么有些基本数据类型的成员变量值会被存储在 常量池中,有些则直接存储在code区

    public class Main {
        int a = 1;
        float b = 1f;
    	float c = 3f;
        double d = 3d;
    	double e = 1d;
    	long f = 2L;
        long g = 1L;
    	int h = 32768;
    
        public static void main(String[] args) {
            //todo
        }
    }
    

    将下述Main类编译成字节码文件后,使用javap -v Main来反编译字节码文件(编译环境为JDK1.8.0_121):

     Last modified 2019-5-3; size 551 bytes
      MD5 checksum 98bc696f571bd00b7bfed6ecc3bff180
      Compiled from "Main.java"
    public class Main
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #17.#38        // java/lang/Object."<init>":()V
       #2 = Fieldref           #16.#39        // Main.a:I
       #3 = Fieldref           #16.#40        // Main.b:F
       #4 = Float              3.0f
       #5 = Fieldref           #16.#41        // Main.c:F
       #6 = Double             3.0d
       #8 = Fieldref           #16.#42        // Main.d:D
       #9 = Fieldref           #16.#43        // Main.e:D
      #10 = Long               2l
      #12 = Fieldref           #16.#44        // Main.f:J
      #13 = Fieldref           #16.#45        // Main.g:J
      #14 = Integer            32768
      #15 = Fieldref           #16.#46        // Main.h:I
      #16 = Class              #47            // Main
      #17 = Class              #48            // java/lang/Object
      #18 = Utf8               a
      #19 = Utf8               I
      #20 = Utf8               b
      #21 = Utf8               F
      #22 = Utf8               c
      #23 = Utf8               d
      #24 = Utf8               D
      #25 = Utf8               e
      #26 = Utf8               f
      #27 = Utf8               J
      #28 = Utf8               g
      #29 = Utf8               h
      #30 = Utf8               <init>
      #31 = Utf8               ()V
      #32 = Utf8               Code
      #33 = Utf8               LineNumberTable
      #34 = Utf8               main
      #35 = Utf8               ([Ljava/lang/String;)V
      #36 = Utf8               SourceFile
      #37 = Utf8               Main.java
      #38 = NameAndType        #30:#31        // "<init>":()V
      #39 = NameAndType        #18:#19        // a:I
      #40 = NameAndType        #20:#21        // b:F
      #41 = NameAndType        #22:#21        // c:F
      #42 = NameAndType        #23:#24        // d:D
      #43 = NameAndType        #25:#24        // e:D
      #44 = NameAndType        #26:#27        // f:J
      #45 = NameAndType        #28:#27        // g:J
      #46 = NameAndType        #29:#19        // h:I
      #47 = Utf8               Main
      #48 = Utf8               java/lang/Object
    {
    //略去字段描述符
      public Main();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: iconst_1
             6: putfield      #2                  // Field a:I
             9: aload_0
            10: fconst_1
            11: putfield      #3                  // Field b:F
            14: aload_0
            15: ldc           #4                  // float 3.0f
            17: putfield      #5                  // Field c:F
            20: aload_0
            21: ldc2_w        #6                  // double 3.0d
            24: putfield      #8                  // Field d:D
            27: aload_0
            28: dconst_1
            29: putfield      #9                  // Field e:D
            32: aload_0
            33: ldc2_w        #10                 // long 2l
            36: putfield      #12                 // Field f:J
            39: aload_0
            40: lconst_1
            41: putfield      #13                 // Field g:J
            44: aload_0
            45: ldc           #14                 // int 32768
            47: putfield      #15                 // Field h:I
            50: return
    		...
    }
    

    通过上述反编译出的Code区和常量池代码,可以很清晰地看出有些基本数据类型值被存储到了常量池,而有些则没有,不同的基本数据类型有略微的差别,但很显然这一切都与jvm指令操作有关。总的来说就是一句话,对于const系列命令和push系列命令操作范围之外的数值类型常量,都放在常量池中。如果想了解具体jvm指令详细解释,可阅读此博客