Java 内部类实现原理简单分析
2015-07-30 10:29
603 查看
转载:原文地址http://www.fzhen.info/?p=300
本文重点不在与内部类的语法及使用,而是试图解释一些背后的原理。
代码清单1:
注意到 Inner 类可以访问 Outer 类的变量,即使是私有变量。那么,显然该内部类是与实例联系的,因为它可以访问实例的变量。对外层实例的显式引用为OuterClass.this。而要去创建某个内部类对象,则必须在new表达式中提供对其外部类对象的引用,使用.new语法,如代码清单2所示:
嵌套类与普通类基本没有区别,除了可以访问其外围类的私有成员而且外围类相当于提供了一个名字空间。
而相对与普通内部类,普通内部类的不能有static的字段和方法,也不能包含嵌套类(但可以有static final的字段)。
内部类的命名为Outer$Inner,对与匿名内部类,会给内部类形成一个编号,生成类似Outer$1.class这样的文件。
Java 8的lambda表达式虽然与内部类渊源极深,但并不会生成单独的字节码文件。
选择一部分输出如下:
可以看到内部类有一个名字为this$0的字段,类型为Outer。 再看内部类的构造函数。我们并没有定义构造函数,但编译器自动生成了一个构造函数,而且带有一个Outer类型的参数。下面逐条看生成的字节码指令:
aload_0: 将局部变量表的第一个变量加载到操作数栈,对于实例方法,该变量是this指针。
aload_1: 将局部变量表第二个变量加载到操作数栈,此处该变量是构造函数的参数,即外围实例引用。
putfield #1: 使用操作数栈顶变量给成员变量赋值。#1是常量池序号,通过查找常量池可知就是编译器自动加上的this$0字段。所以该指令的作用就是把构造函数传进来的Outer实例付给了隐藏的this$0变量。从而内部类获取了外部类的引用。
下面几句调用父类的构造函数并返回。
因为内部类必须持有外围类对象的引用,所以继承内部类时也必须传递外围类对象。Thinking in Java的一个例子:
上面代码在函数foo()中定义了一个内部类,在内部类访问函数局部变量i。两处注释掉的
这与java对闭包的实现有关。编译器为内部类中用到的所有局部变量在内部类中都生成一个对应的字段。当创建内部类的实例时,局部变量的值会通过内部类的构造函数拷贝到内部类的对应字段中。内部类对外围局部变量的访问实际转化成了对内部类字段的访问。
因为值已经被拷贝到了内部类的字段中,那么在外围函数中对该变量的修改会使整个程序的行为变得有点怪异–看起来你可能使用了过期的数据。因此,不能在外围函数中修改变量。对应的,内部类中也不允许修改该变量(实际上,自动生成的字段被声明为final),否则,外围函数好像在处理过期数据。
从上面代码反编译的结果可以清楚看到这一点,见输出中的注释。
本文重点不在与内部类的语法及使用,而是试图解释一些背后的原理。
内部类简介
Java支持在类内部定义类,即为内部类。普通内部类
把类的定义放在类的内部,例如:代码清单1:
public class Outer{ private int outField=10; class Inner{ void innerMethod(){ int i = outField; } } }
注意到 Inner 类可以访问 Outer 类的变量,即使是私有变量。那么,显然该内部类是与实例联系的,因为它可以访问实例的变量。对外层实例的显式引用为OuterClass.this。而要去创建某个内部类对象,则必须在new表达式中提供对其外部类对象的引用,使用.new语法,如代码清单2所示:
public class Outer{ private int outField=10; int foo = 10; class Inner{ int foo = 0; void innerMethod(){ System.out.println(foo);//print 0 System.out.println(Outer.this.foo); //print 10 foo = Outer.this.foo; System.out.println(foo);//print 0 } } public static void main(String[] args){ Outer oc = new Outer(); Outer.Inner ic = oc.new Inner(); ic.innerMethod(); } }
嵌套类
如果不需要内部类对象与外围类之间有联系,那么可以将内部类声明为static。嵌套类与普通类基本没有区别,除了可以访问其外围类的私有成员而且外围类相当于提供了一个名字空间。
而相对与普通内部类,普通内部类的不能有static的字段和方法,也不能包含嵌套类(但可以有static final的字段)。
内部实现
字节码文件
每个内部类被编译成单独的字节码文件,例如编译代码清单1的文件,会生成两个.class文件:$ javac Outer.java $ ls *.class Outer.class Outer$Inner.class
内部类的命名为Outer$Inner,对与匿名内部类,会给内部类形成一个编号,生成类似Outer$1.class这样的文件。
Java 8的lambda表达式虽然与内部类渊源极深,但并不会生成单独的字节码文件。
内部类如何持有外部类实例的引用
使用javap命令反编译内部类文件,$ javap -v Outer\$Inner.class
选择一部分输出如下:
Constant pool: #1 = Fieldref #4.#16 // Outer$Inner.this$0:LOuter; ...其他常量 { final Outer this$0; descriptor: LOuter; flags: ACC_FINAL, ACC_SYNTHETIC Outer$Inner(Outer); descriptor: (LOuter;)V flags: Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:LOuter; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return ...... }
可以看到内部类有一个名字为this$0的字段,类型为Outer。 再看内部类的构造函数。我们并没有定义构造函数,但编译器自动生成了一个构造函数,而且带有一个Outer类型的参数。下面逐条看生成的字节码指令:
aload_0: 将局部变量表的第一个变量加载到操作数栈,对于实例方法,该变量是this指针。
aload_1: 将局部变量表第二个变量加载到操作数栈,此处该变量是构造函数的参数,即外围实例引用。
putfield #1: 使用操作数栈顶变量给成员变量赋值。#1是常量池序号,通过查找常量池可知就是编译器自动加上的this$0字段。所以该指令的作用就是把构造函数传进来的Outer实例付给了隐藏的this$0变量。从而内部类获取了外部类的引用。
下面几句调用父类的构造函数并返回。
因为内部类必须持有外围类对象的引用,所以继承内部类时也必须传递外围类对象。Thinking in Java的一个例子:
class WithInner{ class Inner {} } public class InheritInner extends WithInner.Inner { InheritInner(WithInner wi){ wi.super(); } public static void main(String[] args){ WithInner wi = new WithInner(); InheritInner ii = new InheritInner(wi); } }
内部类只能访问final或相当于final的局部变量
public class Outer2{ void foo(){ int i = 100; class LocalInner{ void bar(){ int d = i; // i++; } } // i++; } }
上面代码在函数foo()中定义了一个内部类,在内部类访问函数局部变量i。两处注释掉的
i++都会产生编译错误:从内部类引用的本地变量必须是最终变量或实际上的最终变量。
这与java对闭包的实现有关。编译器为内部类中用到的所有局部变量在内部类中都生成一个对应的字段。当创建内部类的实例时,局部变量的值会通过内部类的构造函数拷贝到内部类的对应字段中。内部类对外围局部变量的访问实际转化成了对内部类字段的访问。
因为值已经被拷贝到了内部类的字段中,那么在外围函数中对该变量的修改会使整个程序的行为变得有点怪异–看起来你可能使用了过期的数据。因此,不能在外围函数中修改变量。对应的,内部类中也不允许修改该变量(实际上,自动生成的字段被声明为final),否则,外围函数好像在处理过期数据。
从上面代码反编译的结果可以清楚看到这一点,见输出中的注释。
javap -v Outer2\$1LocalInner.class ... class Outer2$1LocalInner ... Constant pool: ... { final int val$i; //生成的字段,对应局部变量i descriptor: I flags: ACC_FINAL, ACC_SYNTHETIC final Outer2 this$0; descriptor: LOuter2; flags: ACC_FINAL, ACC_SYNTHETIC Outer2$1LocalInner(); descriptor: (LOuter2;I)V //构造函数除了Outer2参数,增加了一个int参数 flags: Code: stack=2, locals=3, args_size=3 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:LOuter2; 5: aload_0 6: iload_2 7: putfield #2 // Field val$i:I 把int参数赋值给生成的字段 10: aload_0 11: invokespecial #3 // Method java/lang/Object."<init>":()V 14: return LineNumberTable: line 4: 0 Signature: #15 // ()V void bar(); descriptor: ()V flags: Code: stack=1, locals=2, args_size=1 0: aload_0 1: getfield #2 // Field val$i:I 4: istore_1 // int d = i 对i的访问时机访问的是内部类的字段val$i 5: return LineNumberTable: line 6: 0 line 8: 5 }
$ javap -v Outer2.class ... public class Outer2 ... Constant pool: ... { void foo(); descriptor: ()V flags: Code: stack=4, locals=2, args_size=1 0: bipush 100 2: istore_1 3: new #2 // class Outer2$1LocalInner 6: dup 7: aload_0 //this指针入栈 8: iload_1 //局部变量i入栈 9: invokespecial #3 // Method Outer2$1LocalInner."<init>":(LOuter2;I)V // 调用内部类的构造函数,i作为其中一个参数 12: invokevirtual #4 // Method Outer2$1LocalInner.bar:()V 15: return LineNumberTable: line 3: 0 line 11: 3 line 12: 15 } SourceFile: "Outer2.java" InnerClasses: #7= #2; //LocalInner=class Outer2$1LocalInner
相关文章推荐
- Java String equals和==的详细介绍
- R.layout.main connot be resolved 和R.java消失
- 深入分析Java Web中的中文编码问题
- Java unicode 转中文
- EJB学习(三)——java.lang.ClassCastException: com.sun.proxy.$Proxy2 cannot be cast to..
- java中RSA加解密的实现
- Java SPI机制
- JAVA基础针对自己薄弱环节总结02(循环)
- MyEclipse中使用复制粘贴功能卡的解决办法
- Java 容器中的泛型
- springmvc @RequestParam @RequestBody @PathVariable 等参数绑定注解详解
- Java模式—适配器模式
- Spring获取SessionFactory
- Spring获取SessionFactory
- springmvc请求方法那些事
- Spring获取SessionFactory
- Spring获取SessionFactory
- Spring获取SessionFactory
- java正则过滤html标签属性
- Java串口助手(程序源码)