Java中的final参数/变量+匿名内部类的字节码分析,以及Java 8中的使用
2015-11-27 20:53
676 查看
在Java 8之前一个常识就是如果要在方法中定义一个匿名内部类并使用该方法内的局部变量(包括参数),需要使用final关键字修饰。网上也有很多对这种机制的解释和说明,但是大部分都是一种抽象的认识。如果能够分析一下字节码,这个问题其实很清楚。
显然的一个事实是局部变量(称为变量a)是保存在栈帧的局部变量表中的(引用或基本类型),这里把定义匿名内部类的方法称为方法A,匿名内部类的中使用这个局部变量的方法(称为方法B)经过编译后保存在class文件的方法表中,方法体保存在Code属性中。运行时经过类加载,验证存储到方法区中。在执行匿名内部类中,JVM创建的是方法B的栈帧。它显然和方法A的栈帧没有直接的关系,甚至方法B执行时,方法A的栈帧已经被回收了,那JVM如何保证方法B能够正确的使用变量a指向的值呢?
下面就通过查看字节码来看看Java编译器和JVM是如何做到这一点的,基于Oracle JDK8编译;
定义一个派生自A的匿名内部类,其中使用方法testFinal的局部变量a和参数bytes;
根据内部类命名规则我们知道这个匿名内部类的类名是FinalParameter$1;
使用javap -verbose FinalParameter$1,查看该类的字节码,Unix环境下注意不要忘了转义"$"。
我截取了字段表部分:
可以看到编译器为我们添加了3个字段,分别是:
val$a,val$bytes,this$0;
其中this$0是指向外围类对象实例的引用,而前两个正是从外围类方法局部变量bytes和a生成而来的,并且它们是final的。
再来看看FinalParameter$1类的<init>()方法:
描述符:
Code属性(方法实现):
外围类中testFinal方法中调用匿名内部类的部分:
这里我们就可以清楚看到以下事实:
(1)<init>方法有4个参数,其中第一个是this,另外三个分别类型分别FinalParameter,int,byte数组;
(2)它们参数被赋值给了对应的final字段(this$0,val$a,val$bytes);
(3)外围类将当前实例对象引用,a数值,bytes数组引用传递给了FinalParameter$1的<init>()方法;
因此,我们可以知道,在匿名内部类中使用外围类方法的局部变量,实际上是使用对应值(引用)的final成员变量。
内部类中使用的是该局部变量的副本,因此如果要在语义上保证局部变量和副本的一致性,就应当使用final来保证该局部变量不变。
也就是说,Java中对于内部类使用局部变量的设计思想还要要保证这两个变量(一是局部变量,而是内部类的成员变量)一致性;
如果你不想保证这样的一致性,我觉得可以有以下方案:
(1)可以创建一个抽象类,添加一个构造器,通过构造器传入局部变量的值;
(2)编写一个一个工厂方法(或工厂类),局部变量传入工厂方法(工厂方法的对应参数是final的)返回一个内部类实例;
这样你就不用给局部变量设置为final;
因此,Java设计团队的设计思想是程序员在内部类中使用了局部变量,这种情况应该默认为需要保持两个变量的一致性。
显然的一个事实是局部变量(称为变量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; }
相关文章推荐
- 浅谈java web开发中的乱码问题的解决
- Struts教程(一):搭建Struts环境,初步使用Struts
- 黑马程序员——Eclipse使用教程
- java util
- JAVA设计模式之单例模式
- 一次java全角字符与半角字符转换优化
- java Static解读以及public static void main(String[] args)详细分析
- 001 andorid 目录结构说明
- java Socket编程
- pipeline机制
- java URL的打开如果碰到203会自己重定向
- Java多线程学习笔记(一)
- 第二个spring冲刺第4天
- 深入分析 Java I/O 的工作机制
- Java大数处理
- Spring Web 单元测试实用HelloWorld(附代码)
- Java 结构体排序
- 解决Spring AOP Controller 不生效
- java hdu2088
- hadoop2.5.2 eclipse 插件编译