二.深入理解内部类
1.为什么成员内部类可以无条件访问外部类的成员?
在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,下面是Outter.java的代码:
public class Outter {
private Inner inner = null;
public Outter() {
}
public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}
protected class Inner {
public Inner() {
}
}
}
编译之后,出现了两个字节码文件:
反编译Outter$Inner.class文件得到下面信息:
E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
SourceFile: "Outter.java"
InnerClass:
#24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
minor version: 0
major version: 50
Constant pool:
const #1 = class #2; // com/cxh/test2/Outter$Inner
const #2 = Asciz com/cxh/test2/Outter$Inner;
const #3 = class #4; // java/lang/Object
const #4 = Asciz java/lang/Object;
const #5 = Asciz this$0;
const #6 = Asciz Lcom/cxh/test2/Outter;;
const #7 = Asciz <init>;
const #8 = Asciz (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz Code;
const #10 = Field #1.#11; // com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;// this$0:Lcom/cxh/test2/Outter;
const #12 = Method #3.#13; // java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;// "<init>":()V
const #14 = Asciz ()V;
const #15 = Asciz LineNumberTable;
const #16 = Asciz LocalVariableTable;
const #17 = Asciz this;
const #18 = Asciz Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz SourceFile;
const #20 = Asciz Outter.java;
const #21 = Asciz InnerClasses;
const #22 = class #23; // com/cxh/test2/Outter
const #23 = Asciz com/cxh/test2/Outter;
const #24 = Asciz Inner;
{
final com.cxh.test2.Outter this$0;
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
Code:
Stack=2, Locals=2, Args_size=2
0: aload_0
1: aload_1
2: putfield #10; //Field this$0:Lcom/cxh/test2/Outter;
5: aload_0
6: invokespecial #12; //Method java/lang/Object."<init>":()V
9: return
LineNumberTable:
line 16: 0
line 18: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/cxh/test2/Outter$Inner;
}
第11行到35行是常量池的内容,下面逐一第38行的内容:
final com.cxh.test2.Outter this$0;
这行是一个指向外部类对象的指针,看到这里想必大家豁然开朗了。也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?下面接着看内部类的构造器:
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
正如你所看到的,即使我们是内部类的构造函数定义为一个无参数的构造函数,默认情况下,编译器添加一个参数类型的对象的引用外部类的,所以从这个0指针成员内部类的指向外部类的对象。因此,可以在成员内部类中任意访问外部类的成员。如果不从外部类创建对象,就不能初始化Outter this&0引用并从内部类创建对象。
2. 为什么局部内部类和匿名内部类只能访问局部最终变量?
这个问题困扰了很多人,但是在讨论它之前,让我们看看下面的代码:
public class Test {
public static void main(String[] args) {
}
public void test(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}
}
这段代码会被编译成两个class文件:Test.class和Test1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outter1.class。默认情况下,编译器会为匿名内部类和局部内部类起名为Outterx.class(x为正整数)。
根据上图,测试方法中的匿名内部类的名称叫做test $1。
在前面的代码中,如果a和b之前的任何一个变量被最终删除,则此代码将无法编译。让我们从下面的问题开始:
当测试方法执行时,变量A的生命期结束,但此时Thread对象的生命期还没有结束,因此无法在Thread run方法中继续访问变量A。Java使用复制来解决这个问题。反编译此代码的字节码会产生以下结果:
我们看到在run方法中有一条指令:
bipush 10
该指令推入操作数10,表示使用了一个局部变量。这是由编译器在编译时默认完成的。如果这个变量的值可以在编译时确定,编译器默认会向匿名内部类(局部内部类)的常量池中添加相等的文字,或者直接将相应的字节码嵌入到执行字节码中。这样,匿名内部类使用的变量是另一个局部变量,但其值与方法中的局部变量值相同,所以它与方法中的局部变量是完全分离的。
这是另一个例子:
public class Test {
public static void main(String[] args) {
}
public void test(final int a) {
new Thread(){
public void run() {
System.out.println(a);
};
}.start();
}
}
反编译得到:
匿名内部类Test$1的构造函数接受两个参数:对外部类对象的引用和一个int。匿名内部类Test$1的构造函数接受两个参数:对外部类对象的引用和一个int。匿名内部类Test$1的构造函数接受两个参数:对外部类对象的引用和一个int。
也就是说,如果可以在编译时确定局部变量的值,则直接在匿名内部创建副本。如果在编译时无法确定局部变量的值,则通过向构造函数传递参数初始化副本。
从上面可以看到,在run方法中访问的变量A根本不是测试方法中变量A的本地变量。这解决了前面提到的生命周期不一致的问题。因为run方法中的变量A和测试方法中的变量A是不一样的,当变量A在run方法中被改变时会发生什么?
是的,这会导致数据不一致,不能满足最初的意图和需求。为了解决这个问题,Java编译器将变量A限制为final,并且不允许对变量A进行更改(对于引用类型的变量,不允许指向新对象),从而解决了数据不一致的问题。
在这一点上,应该清楚为什么方法中的局部变量和参数必须用final限定。
3.静态内部类有什么特殊之处吗?
正如您在上一节中看到的,静态内部类独立于外部类,这意味着无需创建外部类的对象,就可以创建内部类的对象。此外,静态内部类不保存对外部类对象的引用。尝试反编译类文件,看看是否有对Outter this&0的引用。