前言
如果你想更多的了解更多的扩展知识,请查阅:
Android中非静态内部类造成内存泄漏
Java虚拟机规范第四章,以了解更多的关于class文件格式
在2的基础上,你还可以查看字节码操作库,比如ASM等。
Java内部类
Java中允许在类中定义另一个类,类里面的类叫做内部类,也叫作嵌套类。同时在内部类里面可以访问外部类的private属性或方法。
public class OuterClass {
private String outer = OuterClass.class.getSimpleName();
private void outer(){
System.out.println("OuterClass outer method invoked...");
}
class InnerClass{
public void print(){
System.out.println(outer);
}
public void inner(){
outer();
}
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
InnerClass inner = outer.new InnerClass();
inner.print();
inner.inner();
}
}
这里内部类InnerClass为什么可以访问OuterClass外部类的private属性或方法呢?
Class文件
找到源文件生成的class文件位置,使用javap命令查看一下生成的两个class文件。
命令行输入:
javap -c OuterClass
得到以下输出:
Compiled from "OuterClass.java"
public class OuterClass {
public OuterClass();
Code:
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #1 // class OuterClass
7: invokevirtual #12 // Method java/lang/Class.getSimpleName:()Ljava/lang/String;
10: putfield #18 // Field outer:Ljava/lang/String;
13: return
public static void main(java.lang.String[]);
Code:
0: new #1 // class OuterClass
3: dup
4: invokespecial #40 // Method "<init>":()V
7: astore_1
8: new #41 // class OuterClass$InnerClass
11: dup
12: aload_1
13: dup
14: invokevirtual #43 // Method java/lang/Object.getClass:()Ljava/lang/Class;
17: pop
18: invokespecial #47 // Method OuterClass$InnerClass."<init>":(LOuterClass;)V
21: astore_2
22: aload_2
23: invokevirtual #50 // Method OuterClass$InnerClass.print:()V
26: aload_2
27: invokevirtual #53 // Method OuterClass$InnerClass.inner:()V
30: return
static java.lang.String access$0(OuterClass);
Code:
0: aload_0
1: getfield #18 // Field outer:Ljava/lang/String;
4: areturn
static void access$1(OuterClass);
Code:
0: aload_0
1: invokespecial #62 // Method outer:()V
4: return
}
可以看到静态access$0和静态access$1方法并不是我们自己定义的,从后面的注释可以了解到这两个方法分别返回了OuterClass的outer属性和outer方法。这两个方法都支持将OuterClass实例作为参数传入。
接下来查看一下内部类的class文件结构。
在命令行中输入:
javap -c OuterClass$InnerClass
得到以下输出:
Compiled from "OuterClass.java"
class OuterClass$InnerClass {
final OuterClass this$0;
OuterClass$InnerClass(OuterClass);
Code:
0: aload_0
1: aload_1
2: putfield #10 // Field this$0:LOuterClass;
5: aload_0
6: invokespecial #12 // Method java/lang/Object."<init>":()V
9: return
public void print();
Code:
0: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #10 // Field this$0:LOuterClass;
7: invokestatic #26 // Method OuterClass.access$0:(LOuterClass;)Ljava/lang/String;
10: invokevirtual #32 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: return
public void inner();
Code:
0: aload_0
1: getfield #10 // Field this$0:LOuterClass;
4: invokestatic #39 // Method OuterClass.access$1:(LOuterClass;)V
7: return
}
在print方法中,会执行access$0方法,目的是拿到外部类的outer属性,如上所述:
invokestatic #26 // Method OuterClass.access$0:(LOuterClass;)Ljava/lang/String;
在inner方法中,会执行access$1方法,目的是拿到外部类的outer方法,如上所述:
invokestatic #39 // Method OuterClass.access$1:(LOuterClass;)V
注意:在内部类构造的时候,会将外部类的引用传递进来,并且作为内部类的一个属性,所以内部类会持有一个其外部类的引用。
this$0就是内部类持有的外部类引用,通过构造方法传递引用并赋值。
外部类访问内部类
public class Outer {
public static void main(String[] args) {
Inner inner = new Outer().new Inner();
System.out.println(inner.x);
}
class Inner {
private int x = 10;
private Inner(){}
}
}
依然采用上面的方式查看class文件,先查看Inner的class文件。
在命令行中输入:
javap -c Outer$Inner
得到以下输出:
Compiled from "Outer.java"
class Outer$Inner {
final Outer this$0;
Outer$Inner(Outer);
Code:
0: aload_0
1: aload_1
2: putfield #12 // Field this$0:LOuter;
5: aload_0
6: invokespecial #14 // Method java/lang/Object."<init>":()V
9: aload_0
10: bipush 10
12: putfield #17 // Field x:I
15: return
static int access$0(Outer$Inner);
Code:
0: aload_0
1: getfield #17 // Field x:I
4: ireturn
}
可以看到内部类提供了一个access$0静态方法来访问属性x,接下来查看Outer的class文件。
在命令行中输入:
javap -c Outer
得到以下输出:
Compiled from "Outer.java"
public class Outer {
public Outer();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #16 // class Outer$Inner
3: dup
4: new #1 // class Outer
7: dup
8: invokespecial #18 // Method "<init>":()V
11: dup
12: invokevirtual #19 // Method java/lang/Object.getClass:()Ljava/lang/Class;
15: pop
16: invokespecial #23 // Method Outer$Inner."<init>":(LOuter;)V
19: astore_1
20: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream;
23: aload_1
24: invokestatic #32 // Method Outer$Inner.access$0:(LOuter$Inner;)I
27: invokevirtual #36 // Method java/io/PrintStream.println:(I)V
30: return
}
外部类通过内部类的实例获取私有属性x的操作如下代码所示:
invokestatic #32 // Method Outer$Inner.access$0:(LOuter$Inner;)I
根据Java官方文档可知,如果(内部类的)成员或构造方法设定成了私有修饰符,当且仅当其外部类访问时是允许的。
如何让内部类私有成员不能被外部类访问?
使用匿名内部类可以避免外部类访问其私有成员。
public class RejectOuter {
Runnable runnable = new Runnable() {
private int x = 10;
@Override
public void run() {
System.out.println(x);
}
};
public static void main(String[] args) {
RejectOuter outer = new RejectOuter();
outer.runnable.run();
// 不允许这样访问
// System.out.println(outer.runnable.x);
}
}
因为runnable属于Runnable类型,而不是匿名内部类类型,而Runnable中没有x这个属性,所以runnable.x是不被允许的。
注意:
使用匿名内部类时,会生成一个对应的class文件。如上述例子中会生成一个 RejectOuter$1文件,在这个文件中会持有外部类的引用。
Compiled from "RejectOuter.java"
class RejectOuter$1 implements java.lang.Runnable {
final RejectOuter this$0;
RejectOuter$1(RejectOuter);
Code:
0: aload_0
1: aload_1
2: putfield #14 // Field this$0:LRejectOuter;
5: aload_0
6: invokespecial #16 // Method java/lang/Object."<init>":()V
9: aload_0
10: bipush 10
12: putfield #19 // Field x:I
15: return
public void run();
Code:
0: getstatic #26 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #19 // Field x:I
7: invokevirtual #32 // Method java/io/PrintStream.println:(I)V
10: return
}
这种写法在Android中可能会引起内存泄漏。
下面是变通写法:
public class MyRunnable implements Runnable{
private int x = 10;
@Override
public void run() {
System.out.println(x);
}
}
public class RejectOuter {
Runnable runnable2 = new MyRunnable();
public static void main(String[] args) {
RejectOuter outer = new RejectOuter();
outer.runnable2.run();
}
}
看到这里相信您在分析Android中非静态内部类造成内存泄漏时会更得心应手。