Java中的final参数/变量+匿名内部类的字节码分析,以及Java 8中的使用

在Java 8之前一个常识就是如果要在方法中定义一个匿名内部类并使用该方法内的局部变量(包括参数),需要使用final关键字修饰。网上也有很多对这种机制的解释和说明,但是大部分都是一种抽象的认识。如果能够分析一下字节码,这个问题其实很清楚。


显然的一个事实是局部变量(称为变量a)是保存在栈帧的局部变量表中的(引用或基本类型),这里把定义匿名内部类的方法称为方法A,匿名内部类的中使用这个局部变量的方法(称为方法B)经过编译后保存在class文件的方法表中,方法体保存在Code属性中。运行时经过类加载,验证存储到方法区中。在执行匿名内部类中,JVM创建的是方法B的栈帧。它显然和方法A的栈帧没有直接的关系,甚至方法B执行时,方法A的栈帧已经被回收了,那JVM如何保证方法B能够正确的使用变量a指向的值呢?


下面就通过查看字节码来看看Java编译器和JVM是如何做到这一点的,基于Oracle JDK8编译;

1. 字节码分析:

外围类:FinalParameter

定义一个派生自A的匿名内部类,其中使用方法testFinal的局部变量a和参数bytes;

public final class FinalParameter {
    interface A {
        void test();
    }
    private void testFinal(final byte[] bytes) {
        final int a = 10;
        new A() {
            @Override
            public void test() {
                System.out.println(a + " " + bytes.length);
            }
        };
    }
}

根据内部类命名规则我们知道这个匿名内部类的类名是FinalParameter$1;

使用javap -verbose FinalParameter$1,查看该类的字节码,Unix环境下注意不要忘了转义"$"。

我截取了字段表部分:

  final int val$a;
    descriptor: I
    flags: ACC_FINAL, ACC_SYNTHETIC

  final byte[] val$bytes;
    descriptor: [B
    flags: ACC_FINAL, ACC_SYNTHETIC

  final com.jvm.showByteCode.FinalParameter this$0;
    descriptor: Lcom/jvm/showByteCode/FinalParameter;
    flags: ACC_FINAL, ACC_SYNTHETIC

可以看到编译器为我们添加了3个字段,分别是:

val$a,val$bytes,this$0;

其中this$0是指向外围类对象实例的引用,而前两个正是从外围类方法局部变量bytes和a生成而来的,并且它们是final的。

再来看看FinalParameter$1类的<init>()方法:

描述符:

descriptor: (Lcom/jvm/showByteCode/FinalParameter;I[B)V

Code属性(方法实现):

Code:
      stack=2, locals=4, args_size=4
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field this$0:Lcom/jvm/showByteCode/FinalParameter;
         5: aload_0
         6: iload_2
         7: putfield      #2                  // Field val$a:I
        10: aload_0
        11: aload_3
        12: putfield      #3                  // Field val$bytes:[B
        15: aload_0
        16: invokespecial #4                  // Method java/lang/Object."<init>":()V
        19: return

外围类中testFinal方法中调用匿名内部类的部分:

10: invokespecial #3                  // Method com/jvm/showByteCode/FinalParameter$1."<init>":(Lcom/jvm/showByteCode/FinalParameter;I[B)V

这里我们就可以清楚看到以下事实:

(1)<init>方法有4个参数,其中第一个是this,另外三个分别类型分别FinalParameter,int,byte数组;

(2)它们参数被赋值给了对应的final字段(this$0,val$a,val$bytes);

(3)外围类将当前实例对象引用,a数值,bytes数组引用传递给了FinalParameter$1的<init>()方法;


因此,我们可以知道,在匿名内部类中使用外围类方法的局部变量,实际上是使用对应值(引用)的final成员变量。

内部类中使用的是该局部变量的副本,因此如果要在语义上保证局部变量和副本的一致性,就应当使用final来保证该局部变量不变。


2. Java 8中“改进”:

下段代码通过了基于JDK8的编译,但是如果将注释去掉,仍然不能通过编译,看来只是在写法上“省力”了。
也就是说,Java中对于内部类使用局部变量的设计思想还要要保证这两个变量(一是局部变量,而是内部类的成员变量)一致性;
如果你不想保证这样的一致性,我觉得可以有以下方案:
(1)可以创建一个抽象类,添加一个构造器,通过构造器传入局部变量的值;
(2)编写一个一个工厂方法(或工厂类),局部变量传入工厂方法(工厂方法的对应参数是final的)返回一个内部类实例;
这样你就不用给局部变量设置为final;
因此,Java设计团队的设计思想是程序员在内部类中使用了局部变量,这种情况应该默认为需要保持两个变量的一致性。
private void testFinal(byte[] bytes) {
        int a = 10;
        new A() {
            @Override
            public void test() {
                System.out.println(a + " " + bytes.length);
            }
        };
//        a = 11;
//        bytes = null;
    }






  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值