好文转载:http://blog.****.net/zhangjg_blog/article/details/19996629
开篇
在我的上一篇博客 深入理解Java中为什么内部类可以访问外部类的成员 中, 通过使用javap工具反编译内部类的字节码, 我们知道了为什么内部类中可以访问外部类的成员, 其实是编译器在编译内部类的class文件时,偷偷做了一些工作, 使内部类持有外部类的引用, 并且通过在构造方法上添加参数注入这个引用, 在调用构造方法时默认传入了外部类的引用。 我们之所以感到疑惑, 就是因为编译器使用的障眼法。当我们把字节码反编译出来之后, 编译器的这些小伎俩就会清清楚楚的展示在我们面前。 感兴趣的朋友可以移步到上一篇博客, 博客链接: http://blog.****.NET/zhangjg_blog/article/details/20000769
在本文中, 我们要对定义在方法中的内部类进行分析。 和上一篇博客一样, 我们还是使用javap工具对内部类的字节码进行解剖。 并且和上一篇文章进行对比分析, 探究定义在外部类方法中的内部类和定义在外部类中的内部类有哪些相同之处和不同之处。 这篇博客的讲解以上一篇为基础, 对这些知识点不是很熟悉的同学, 强烈建议先读上一篇博客。 博客的链接已经在上面给出。
定义在方法中的内部类
在平时写代码的过程中, 我们经常会写类似下面的代码段:
- public class Test {
- public static void main(String[] args) {
- final int count = 0;
- new Thread(){
- public void run() {
- int var = count;
- };
- }.start();
- }
- }
这段代码在main方法中定义了一个匿名内部类, 并且创建了匿名内部类的一个对象, 使用这个对象调用了匿名内部类中的方法。 所有这些操作都在new Thread(){}.start() 这一句代码中完成, 这不禁让人感叹java的表达能力还是很强的。 上面的代码和以下代码等价:
- public class Test {
- public static void main(String[] args) {
- final int count = 0;
- //在方法中定义一个内部类
- class MyThread extends Thread{
- public void run() {
- int var = count;
- }
- }
- new MyThread().start();
- }
- }
这里我们不关心方法中匿名内部类和非匿名内部类的区别, 我们只需要知道, 这两种方式都是定义在方法中的内部类, 他们的工作原理是相同的。 在本文中主要根据非匿名内部类讲解。
让我们仔细观察上面的代码都有哪些“奇怪”的行为:
1 在外部类的main方法中有一个局部变量count, 并且在内部类的run方法中访问了这个count变量。 也就是说, 方法中定义的内部类, 可以访问方法中的局部变量(方法的参数也是局部变量);
2 count变量使用final关键字修饰, 如果去掉final, 则编译失败。 也就是说被方法中的内部类访问的局部变量必须是final的。
由于我们经常这样做, 这样写代码, 久而久之养成了习惯, 就成了司空见惯的做法了。 但是如果要问为什么Java支持这样的做法, 恐怕很少有人能说的出来。 在下面, 我们就会分析为什么Java支持这种做法, 让我们不仅知其然, 还要知其所以然。
为什么定义在方法中的内部类可以访问方法中的局部变量?
1 当被访问的局部变量是编译时可确定的字面常量时
- public class Outer {
- void outerMethod(){
- final String localVar = "abc";
- /*定义在方法中的内部类*/
- class Inner{
- void innerMethod(){
- String a = localVar;
- }
- }
- }
- }
在外部类的方法outerMethod中定义了成员变量 String localVar, 并且用一个编译时字面量"abc"给他赋值。在 outerMethod方法中定义了内部类Inner, 并且在内部类的方法innerMethod中访问了localVar变量。 接下来我们就根据这个例子来讲解为什么可以这样做。
- Constant pool:
- #1 = Class #2 // Outer$1Inner
- #2 = Utf8 Outer$1Inner
- #3 = Class #4 // java/lang/Object
- #4 = Utf8 java/lang/Object
- #5 = Utf8 this$0
- #6 = Utf8 LOuter;
- #7 = Utf8 <init>
- #8 = Utf8 (LOuter;)V
- #9 = Utf8 Code
- #10 = Fieldref #1.#11 // Outer$1Inner.this$0:LOuter;
- #11 = NameAndType #5:#6 // this$0:LOuter;
- #12 = Methodref #3.#13 // java/lang/Object."<init>":()V
- #13 = NameAndType #7:#14 // "<init>":()V
- #14 = Utf8 ()V
- #15 = Utf8 LineNumberTable
- #16 = Utf8 LocalVariableTable
- #17 = Utf8 this
- #18 = Utf8 LOuter$1Inner;
- #19 = Utf8 innerMethod
- #20 = String #21 // abc
- #21 = Utf8 abc
- #22 = Utf8 a
- #23 = Utf8 Ljava/lang/String;
- #24 = Utf8 SourceFile
- #25 = Utf8 Outer.java
- #26 = Utf8 EnclosingMethod
- #27 = Class #28 // Outer
- #28 = Utf8 Outer
- #29 = NameAndType #30:#14 // outerMethod:()V
- #30 = Utf8 outerMethod
- #31 = Utf8 InnerClasses
- #32 = Utf8 Inner
- {
- final Outer this$0;
- flags: ACC_FINAL, ACC_SYNTHETIC
- Outer$1Inner(Outer);
- flags:
- Code:
- stack=2, locals=2, args_size=2
- 0: aload_0
- 1: aload_1
- 2: putfield #10 // Field this$0:LOuter;
- 5: aload_0
- 6: invokespecial #12 // Method java/lang/Object."<init>":()V
- 9: return
- LineNumberTable:
- line 8: 0
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 10 0 this LOuter$1Inner;
- void innerMethod();
- flags:
- Code:
- stack=1, locals=2, args_size=1
- 0: ldc #20 // String abc
- 2: astore_1
- 3: return
- LineNumberTable:
- line 10: 0
- line 11: 3
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 4 0 this LOuter$1Inner;
- 3 1 1 a Ljava/lang/String;
- }
innerMethod方法中一共就以下有三个指令:
2: astore_1
3: return
- ......
- ......
- #13 = Utf8 LOuter;
- #14 = Utf8 outerMethod
- #15 = String #16 // abc
- #16 = Utf8 abc
- ......
- ......
我们可以看到, “abc”这个字符串确实出现在Outer.class常量池的第15项。 这就奇怪了, 明明是定义在外部类的字面量, 为什么会出现在 内部类的常量池中呢? 其实这正是编译器在编译方法中定义的内部类时, 所做的额外工作。
- public class Outer {
- void outerMethod(){
- final int localVar = 1;
- /*定义在方法中的内部类*/
- class Inner{
- void innerMethod(){
- int a = localVar;
- }
- }
- }
- }
内部类反编译后的class文件如下: (由于在这里常量池不是重点, 所以省略了常量池信息)
- {
- final Outer this$0;
- flags: ACC_FINAL, ACC_SYNTHETIC
- Outer$1Inner(Outer);
- flags:
- Code:
- stack=2, locals=2, args_size=2
- 0: aload_0
- 1: aload_1
- 2: putfield #10 // Field this$0:LOuter;
- 5: aload_0
- 6: invokespecial #12 // Method java/lang/Object."<init>":()V
- 9: return
- LineNumberTable:
- line 8: 0
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 10 0 this LOuter$1Inner;
- void innerMethod();
- flags:
- Code:
- stack=1, locals=2, args_size=1
- 0: iconst_1
- 1: istore_1
- 2: return
- LineNumberTable:
- line 10: 0
- line 11: 2
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 3 0 this LOuter$1Inner;
- 2 1 1 a I
- }
从上面的输出可以看到, innerMethod方法中的第一句字节码为:
- iconst_1
这句字节码的意义是:将int类型的常量 1 压入操作数栈。 这就是在内部类中访问外部类方法中的局部变量int localVar = 1的原理。 由此可见, 当内部类中访问的局部变量是int型的字面量时, 编译器直接将对该变量的访问嵌入到内部类的字节码中, 也就是说, 在运行时, 方法中的内部类和外部类, 和外部类方法中的局部变量就没有任何关系了。 这也是编译器所做的额外工作。
- final String localVar = "abc";
- final int localVar = 1;
他们之所以被称为字面常量, 是因为他们被final修饰, 运行时不可改变, 当编译器在编译源文件时, 可以确定他们的值, 也可以确定他们在运行时不会被修改, 所以可以实现类似C语言宏替换的功能。也就是说虽然在编写源代码时, 在另一个类中访问的是当前类定义的这个变量, 但是在编译成字节码时, 却把这个变量的值放入了访问这个变量的另一个类的常量池中, 或直接将这个变量的值嵌入另一个类的字节码指令中。 运行时这两个类各不相干, 各自访问各自的常量池, 各自执行各自的字节码指令。在编译方法中定义的内部类时, 编译器的行为就是这样的。
2 当被访问的局部变量的值在编译时不可确定时
- public class Outer {
- void outerMethod(){
- final String localVar = getString();
- /*定义在方法中的内部类*/
- class Inner{
- void innerMethod(){
- String a = localVar;
- }
- }
- new Inner();
- }
- String getString(){
- return "aa";
- }
- }
由于使用getString方法的返回值为localVar赋值, 所以在编译时期, 编译器不可确定localVar的值, 必须在运行时执行了getString方法之后才能确定它的值。 既然编译时不不可确定, 那么像上面那样的处理就行不通了。 那么在这种情况下, 内部类是通过什么机制访问方法中的局部变量的呢? 让我们继续反编译内部类的字节码:
- Constant pool:
- #1 = Class #2 // Outer$1Inner
- #2 = Utf8 Outer$1Inner
- #3 = Class #4 // java/lang/Object
- #4 = Utf8 java/lang/Object
- #5 = Utf8 this$0
- #6 = Utf8 LOuter;
- #7 = Utf8 val$localVar
- #8 = Utf8 Ljava/lang/String;
- #9 = Utf8 <init>
- #10 = Utf8 (LOuter;Ljava/lang/String;)V
- #11 = Utf8 Code
- #12 = Fieldref #1.#13 // Outer$1Inner.this$0:LOuter;
- #13 = NameAndType #5:#6 // this$0:LOuter;
- #14 = Fieldref #1.#15 // Outer$1Inner.val$localVar:Ljava/la
- ng/String;
- #15 = NameAndType #7:#8 // val$localVar:Ljava/lang/String;
- #16 = Methodref #3.#17 // java/lang/Object."<init>":()V
- #17 = NameAndType #9:#18 // "<init>":()V
- #18 = Utf8 ()V
- #19 = Utf8 LineNumberTable
- #20 = Utf8 LocalVariableTable
- #21 = Utf8 this
- #22 = Utf8 LOuter$1Inner;
- #23 = Utf8 innerMethod
- #24 = Utf8 a
- #25 = Utf8 SourceFile
- #26 = Utf8 Outer.java
- #27 = Utf8 EnclosingMethod
- #28 = Class #29 // Outer
- #29 = Utf8 Outer
- #30 = NameAndType #31:#18 // outerMethod:()V
- #31 = Utf8 outerMethod
- #32 = Utf8 InnerClasses
- #33 = Utf8 Inner
- {
- final Outer this$0;
- flags: ACC_FINAL, ACC_SYNTHETIC
- Outer$1Inner(Outer, java.lang.String);
- flags:
- Code:
- stack=2, locals=3, args_size=3
- 0: aload_0
- 1: aload_1
- 2: putfield #12 // Field this$0:LOuter;
- 5: aload_0
- 6: aload_2
- 7: putfield #14 // Field val$localVar:Ljava/lang/String;
- 10: aload_0
- 11: invokespecial #16 // Method java/lang/Object."<init>":()V
- 14: return
- LineNumberTable:
- line 8: 0
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 15 0 this LOuter$1Inner;
- void innerMethod();
- flags:
- Code:
- stack=1, locals=2, args_size=1
- 0: aload_0
- 1: getfield #14 // Field val$localVar:Ljava/lang/String;
- 4: astore_1
- 5: return
- LineNumberTable:
- line 10: 0
- line 11: 5
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 6 0 this LOuter$1Inner;
- 5 1 1 a Ljava/lang/String;
- }
首先来看它的构造方法。 方法的签名为:
- Outer$1Inner(Outer, java.lang.String);
我们只到, 如果不定义构造方法, 那么编译器会为这个类自动生成一个无参数的构造方法。 这个说法在这里就行不通了, 因为我们看到, 这个内部类的构造方法又两个参数。 至于第一个参数, 是指向外部类对象的引用, 在前面一篇博客中已经详细的介绍过了, 不明白的可以先看上一篇博客, 这里就不再重复叙述。这也说明了方法中的内部类和类中定义的内部类有相同的地方, 既然他们都是内部类, 就都持有指向外部类对象的引用。 我们来分析第二个参数, 他是String类型的, 和在内部类中访问的局部变量localVar的类型相同。 再看构造方法中编号为6和7的字节码指令:
- 6: aload_2
- 7: putfield #14 // Field val$localVar:Ljava/lang/String;
- 0: aload_0
- 1: getfield #14 // Field val$localVar:Ljava/lang/String;
这两条指令的意思是, 访问成员变量val$localVar的值。 而源代码中是访问外部类方法中局部变量的值。 所以, 在这里将编译时对外部类方法中的局部变量的访问, 转化成运行时对当前内部类对象中成员变量的访问。
在源代码层面上, 它的工作方式有点像这样: (注意, 下面的代码不符合Java的语法, 只是模拟编译器的行为)
- public class Outer {
- void outerMethod(){
- final String localVar = getString();
- /*定义在方法中的内部类*/
- class Inner{
- /*下面两个成员变量都是编译器自动加上的*/
- final Outer this$0; //指向外部类对象的引用
- final String val$localVar; //被访问的外部类方法中的局部变量的值
- /*构造方法, 两个参数都是编译器添加的*/
- public Inner(Outer outer, String outerMethodLocal){
- this.this$0 = outer;
- this.val$localVar = outerMethodLocal;
- super();
- }
- void innerMethod(){
- /*将对外部类方法中的变量的访问, 转换成对当前对象的成员变量的访问*/
- //String a = localVar;
- String a = val$localVar;
- }
- }
- /*在外部类方法中创建内部类对象时, 传入相应的参数,
- 这两个参数分别是当前外部类的引用, 和当前方法中的局部变量*/
- //new Inner();
- new Inner(this, localVar);
- }
- String getString(){
- return "aa";
- }
- }
讲到这里, 内部类的行为就比较清晰了。 总结一下就是: 当方法中定义的内部类访问的方法局部变量的值, 不是在编译时能确定的字面常量时, 编译器会为内部类增加一个成员变量, 在运行时, 将对外部类方法中局部变量的访问。 转换成对这个内部类成员变量的方法。 这就要求内部类中的这个新增的成员变量和外部类方法中的局部变量具有相同的值。 编译器通过为内部类的构造方法增加参数, 并在调用构造器初始化内部类对象时传入这个参数, 来初始化内部类中的这个成员变量的值。 所以, 虽然在源文件中看起来是访问的外部类方法的局部变量, 其实运行时访问的是内部类对象自己的成员变量。
为什么被方法内的内部类访问的局部变量必须是final的
- public class Outer {
- void outerMethod(){
- final int localVar = getInt();
- /*定义在方法中的内部类*/
- class Inner{
- void innerMethod(){
- int a = localVar;
- }
- }
- new Inner();
- }
- int getInt(){ return 1; }
- }
如果这个局部变量是引用数据类型时, 拷贝外部类方法中的引用值给内部类对象的成员变量, 这样的话, 他们就指向了同一个对象。 代码示例和运行时的内存布局如下:
- public class Outer {
- void outerMethod(){
- final Person localVar = getPerson();
- /*定义在方法中的内部类*/
- class Inner{
- void innerMethod(){
- Person a = localVar;
- }
- }
- new Inner();
- }
- Person getPerson(){ return new Person("zhangjg", 30); }
- }
由于这两个引用变量指向同一个对象, 所以通过引用访问的对象的数据是一样的, 由于他们都不能再指向其他对象(被final修饰), 所以可以保证内部类和外部类数据访问的一致性。