首先解释一下什么是内部类,内部类是定义在一个类内部的类。分为两种情况:一种是被static关键字修饰的, 叫做静态内部类, 另一种是不被static关键字修饰的, 即是普通内部类。 在下文中所提到的内部类都是指这种不被static关键字修饰的普通内部类。 静态内部类虽然也定义在外部类的里面, 但是它只是在形式上(写法上)和外部类有关系, 其实在逻辑上和外部类并没有直接的关系。而一般的内部类,不仅在形式上和外部类有关系(写在外部类的里面), 在逻辑上也和外部类有联系。 这种逻辑上的关系可以总结为以下两点:
1. 内部类对象的创建依赖于外部类对象;
2. 内部类对象持有指向外部类对象的引用。
上边的第二条可以解释为什么在内部类中可以访问外部类的成员。就是因为内部类对象持有外部类对象的引用。 为什么会持有这个引用?Java为了语法的简洁, 省略了很多该写的东西, 也就是说很多东西本来应该在源代码中写出, 但是为了方便起见, 不必在源码中写出,编译器在编译时会加上一些代码。
举个内部例子:
public class Outer {
int outerField = 0;
class Inner{
void InnerMethod(){
int i = outerField;
}
}
}
在外部类Outer中定义了内部类Inner, 并且在Inner的方法中访问了Outer的成员变量outerField。虽然这两个类写在同一个文件中, 但是编译完成后, 还是生成各自的class文件。反编译内部类的class文件Outer$Inner.class :
{
final Outer this$0;
flags: ACC_FINAL, ACC_SYNTHETIC
Outer$Inner(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."
9: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this LOuter$Inner;
void InnerMethod();
flags:
Code:
stack=1, locals=2, args_size=1
0: aload_0
1: getfield #10 // Field this$0:LOuter;
4: getfield #20 // Field Outer.outerField:I
7: istore_1
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LOuter$Inner;
8 1 1 i I
}
解释一下这里的几条语句:
final Outer this$0; :意思是在内部类Outer$Inner中, 存在一个名字为this$0 , 类型为Outer的成员变量, 并且这个变量是final的。 这个就是所谓的“在内部类对象中存在的指向外部类对象的引用”。但是我们在定义这个内部类的时候 并没有声明它, 也就是说这个成员变量是编译器加上的。 虽然编译器在创建内部类时为它加上了一个指向外部类的引用, 但是这只是一个引用,关键在于它何时进行指向并且指向谁。也就是对其进行赋值。
下面我们把注意力转移到构造函数上。 下面这段输出是关于构造函数的信息。
Outer$Inner(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."
9: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this LOuter$Inner;
这里有一个构造方法且类型为Outer。 编译器会为内部类的构造方法添加一个参数, 参数的类型就是外部类的类型。构造参数中如何使用这个默认添加的参数。 分析一下构造方法的字节码。 下面是每行字节码的意义:
aload_0 : 将局部变量表中的第一个引用变量加载到操作数栈。 局部变量表中的变量在方法执行前就已经初始化完成;局部变量表中的变量包括方法的参数;成员方法的局部变量表中的第一个变量永远是this;操作数栈就是执行当前代码的栈。所以这句话的意思是: 将this引用从局部变量表加载到操作数栈。
aload_1:将局部变量表中的第二个引用变量加载到操作数栈。 这里加载的变量就是构造方法中的Outer类型的参数。
putfield #10:使用操作数栈顶端的引用变量为指定的成员变量赋值。 这里的意思是将外面传入的Outer类型的参数赋给成员变量this$0 。 这一句putfield字节码就揭示了, 指向外部类对象的这个引用变量是如何赋值的。
下面几句字节码的含义是: 使用this引用调用父类(Object)的构造方法然后返回。
构造函数类似于:
class Outer$Inner{
final Outer this$0;
public Outer$Inner(Outer outer){
this.this$0 = outer;
super();
}
}
这时其实可以发现编译器不仅增加了常量引用还修改了构造函数,同时还增加了三个静态方法。这个常量引用准备指向外部类,并通过修改构造方法,传递一个OutClass类型的参数进去,这样虽然内部类拿到了外部类的引用,但是对于虚拟机来看这就是两个不相关的类,所以还是不能在一个类中调用另外一个类的私有属性。
在调用外部类的私有方法的时候,会调用内部类自动生成的常量指针以及外部类自动生成的三个静态方法。可以看出这三个静态方法都是返回外部类对应的私有属性,编译器很智能会扫描内部类,查看是否调用了外部类的私有属性,只有调用了才会生成access$xxx方法,在虚拟机中没有外部类内部类之分都是普通的类,但是编译器会偷偷的做点修改,让内部类中多一个常量引用指向外部类,自动修改内部类构造器,初始化这个常量引用,而外部类通过扫描内部类调用了外部类的那些私有属性,为这些私有属性创造acess$xxx静态方法。
现在简单了解一下由编译器生成的静态方法acess$xxx。static Type access$ iii (Outer);这个方法是返回对应的私有属性的值。所以可以在一个类的外部获取一个类的私有属性的值。 是JAVA编译器自动生成的十分重要的方法(该方法的个数由你的内部类要访问的外部类的变量个数相关),目的是用于内部类访问外部类的数据成员时使用 . JAVA编译器在生成内部类的访问外部类的数据成员时,会 自动生成代码来调用这个方法 . 这种内部类访问外部类中 private数据成员 的技术( 不是通过反射) 给安全留下了 可能的隐患 (因为有些private数据成员是不提供外界访问它的所谓的getter()的)。为此,编译器对 自己自动生成的 这些access$000()方法,在编译时进行检查,是不允许程序员直接来调用的。
为了使内部类访问外部类的私有成员,编译器生成了形似 “外部类.access$XYZ”的函数。XYZ为数字。X是按照私有成员在内部类出现的顺序递增的。YZ为02的话,标明是基本变量成员;YZ为00的话标明是对象成员或者函数。
内部类会隐式持有外部类的引用,而静态内部类不是持有外部类的引用。其原理时Java编译器在编译时会把静态类编译成外部类一样的顶级类。非静态内部类,在编译完成后,在字节码文件中会生成一个外部类类型的this$0的引用。我们知道类的私有变量和方法,其他类是无法访问到的。既然都是外部类,那么内部类访问外部类的私有变量和私有方法是怎么实现的呢?答案是在编译期间自动为外部类生成了access$xxx的方法来封装外部类的私有成员和方法已供内部类使用。匿名内部类和非匿名内部类相比,匿名内部类是没有名字的,在编译期间,Java编译器会根据匿名内部类在外部类出现的顺序,生成命名为外部类&number的类。number会依次累加。
我们可以利用JAVA编译器对类的编译特性来 绕过这个检查 ,目的是,达到在自己的其它类中直接来调用这些access$000()方法. 这样,我们可采用这个技术(即:在自己的类中-- 注意不是内部类,而是外部类中 直接来调用这个access$000(Outer);)来访问其它类的private的数据成员了. 演示如下:
第一步:定义如下的类:
class Outer {
private final int xx = 123;
//由于是final,故 不再自动生成 access$000(Outer);
public Inner getInner() {
return new Inner();
}
public class Inner {
public int getDate() {
return xx;
}
} //class Inner
static int access$000(Outer)// 这个是自已定义的!
{
return 1;
}
第二步:定义你的其它类,来直接调用这个access$000()方法
public class Test1
{
public static void main(String[] args)
{
System.out.println(Outer.access$000(new Outer())); //这个调用是没有问题的,因为是自己定义的!
}
}
将上述两个JAVA文件编译成class,成其是第二步的 Test1.class
第三步:这是变戏法的一步:
将第一步的类Outer改为如下:
class Outer {
private int xx = 123;
//由于不是final,故 自动生成 access$000(Outer);
public Inner getInner() {
return new Inner();
}
public class Inner {
public int getDate() {
return xx;
}
} //class Inner
/*将这个 第一步中自己定义的access$000去掉,因为编译器会自动生成它!
static int access$000(Outer {
return 1;
} */
重新编译第三步中的这个类,而第二步中的类Test.class不动它. 此时,我们达到了这样一个目的:在类Test1中调用了Outer类中编译器自动生成的这个access$000(...)了.
1 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象的引用;
2 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为1中添加的成员变量赋值;
3 在调用内部类的构造函数初始化内部类对象时, 会默认传入外部类的引用。