Java中有5种内部类,公有静态内部类,私有静态内部类,公有内部类,私有内部类,匿名内部类。这里我们不谈各种内部类的使用场景,不对比优劣,仅从语法糖的角度学习Java是怎么实现内部类的。
1.静态内部类
静态内部类的初始化,不依赖于外部类的实例,并且不能访问外部类的实例变量,不能调用外部类的实例方法。
公有静态内部类
测试代码如下所示:
public class TestPubStaticInnerClass {
private static int priStaticValue = 3;
public static int pubStaticValue = 3;
private static void priFOut() {
}
public static void pubFOut() {
}
public static class PubStaticInnerClass {
private int fPubStaticInnerClass() {
priFOut();
pubFOut();
return priStaticValue + pubStaticValue;
}
}
public static void main(String[] args) {
PubStaticInnerClass pubStaticInnerClass = new PubStaticInnerClass();
}
}
运行命令:
//注意参数--removeinnerclasssynthetics false,否则反编译后的代码会重新糖化
java -jar cfr-0.145.jar TestPubStaticInnerClass.class --removeinnerclasssynthetics false
将得到如下代码:
/*
* Decompiled with CFR 0.145.
*/
public class TestPubStaticInnerClass {
private static int priStaticValue = 3;
public static int pubStaticValue = 3;
private static void priFOut() {
}
public static void pubFOut() {
}
public static void main(String[] args) {
PubStaticInnerClass pubStaticInnerClass = new PubStaticInnerClass();
}
static /* synthetic */ void access$000() {
TestPubStaticInnerClass.priFOut();
}
static /* synthetic */ int access$100() {
return priStaticValue;
}
public static class PubStaticInnerClass {
private int fPubStaticInnerClass() {
TestPubStaticInnerClass.access$000();
TestPubStaticInnerClass.pubFOut();
return TestPubStaticInnerClass.access$100() + pubStaticValue;
}
}
}
可以看到,对于外部类的公有静态属性或方法,内部类可以直接调用。对于私有静态属性或者方法,编译器将会为其生成一个包级可见域的静态方法,如access$000和access$100,供内部类调用。
私有静态内部类
测试代码如下所示:
public class TestPriStaticInnerClass {
private static int priStaticValue = 3;
public static int pubStaticValue = 3;
private static void priFOut() {
}
public static void pubFOut() {
}
private static class PriStaticInnerClass {
private int fPubStaticInnerClass() {
priFOut();
pubFOut();
return priStaticValue + pubStaticValue;
}
}
public static void main(String[] args) {
PriStaticInnerClass priStaticInnerClass = new PriStaticInnerClass();
}
}
私有静态内部类在访问外部类的静态属性或方法的逻辑,与公有静态内部类的逻辑是一致的,不同的地方在于编译器对构造函数的处理。 私有静态内部类的默认构造函数是私有的,编译器为此额外生成了一个无逻辑内部类TestPriStaticInnerClass$1.class和一个以此类为参数的包级可见域的构造函数, 通过传入null调用新生成的带参数的构造函数。反编译后的代码如下所示:
/*
* Decompiled with CFR 0.145.
*/
public class TestPriStaticInnerClass {
private static int priStaticValue = 3;
public static int pubStaticValue = 3;
private static void priFOut() {
}
public static void pubFOut() {
}
public static void main(String[] args) {
PriStaticInnerClass priStaticInnerClass = new PriStaticInnerClass(null);
}
static /* synthetic */ void access$000() {
TestPriStaticInnerClass.priFOut();
}
static /* synthetic */ int access$100() {
return priStaticValue;
}
private static class PriStaticInnerClass {
private PriStaticInnerClass() {
}
private int fPubStaticInnerClass() {
TestPriStaticInnerClass.access$000();
TestPriStaticInnerClass.pubFOut();
return TestPriStaticInnerClass.access$100() + pubStaticValue;
}
/* synthetic */ PriStaticInnerClass(1 x0) {
this();
}
}
}
2.非静态内部类
非静态内部类的初始化,依赖于外部类的实例,会用一个实例变量保存外部类实例。内部类可以访问外部类的所有实例属性、方法及静态属性、方法。
公有内部类
测试代码如下所示:
public class TestPubInnerClass {
private int privateValue = 0;
private static int privateStaticValue = 0;
public int publicValue = 1;
public static int publicStaticValue = 3;
public static void outPubStaticF() {
}
private static void outPriStaticF() {
}
public void outPubF() {
}
private void outPriF() {
}
public class PubInnerClass {
public int f() {
outPubStaticF();
outPriStaticF();
outPubF();
outPriF();
return privateValue + privateStaticValue + publicValue + publicStaticValue;
}
}
public static void main(String[] args) {
TestPubInnerClass testPubInnerClass = new TestPubInnerClass();
PubInnerClass pubInnerClass = testPubInnerClass.new PubInnerClass();
}
}
反编译后的代码如下所示:
/*
* Decompiled with CFR 0.145.
*/
public class TestPubInnerClass {
private int privateValue = 0;
private static int privateStaticValue = 0;
public int publicValue = 1;
public static int publicStaticValue = 3;
public static void outPubStaticF() {
}
private static void outPriStaticF() {
}
public void outPubF() {
}
private void outPriF() {
}
public static void main(String[] args) {
TestPubInnerClass testPubInnerClass = new TestPubInnerClass();
PubInnerClass pubInnerClass = new PubInnerClass(testPubInnerClass);
}
static /* synthetic */ void access$000() {
TestPubInnerClass.outPriStaticF();
}
static /* synthetic */ void access$100(TestPubInnerClass x0) {
x0.outPriF();
}
static /* synthetic */ int access$200(TestPubInnerClass x0) {
return x0.privateValue;
}
static /* synthetic */ int access$300() {
return privateStaticValue;
}
public class PubInnerClass {
final /* synthetic */ TestPubInnerClass this$0;
public PubInnerClass(TestPubInnerClass this$0) {
this.this$0 = this$0;
}
public int f() {
TestPubInnerClass.outPubStaticF();
TestPubInnerClass.access$000();
this.this$0.outPubF();
TestPubInnerClass.access$100(this.this$0);
return TestPubInnerClass.access$200(this.this$0) + TestPubInnerClass.access$300() + this.this$0.publicValue + publicStaticValue;
}
}
}
可以看到,对于静态属性和方法的处理,与静态内部类的逻辑是一致的。 对于实例属性和方法,编译器同样为外部类添加了包级可见域的静态方法access$100和access$200,内部类调用方法时,会将外部类实例传参给方法,实现间接调用。
私有内部类
私有内部类在属性和方法上与公有内部类的处理逻辑一致,除此之外,编译器会额外为私有内部类添加一个构造函数,与私有静态内部类的处理逻辑一致,这里不再赘述。
3.匿名内部类
匿名内部类的情况比上述内部类稍有不同,不过也是有迹可循。匿名内部类不像内部类一样,有公有私有一说,只有静态或非静态、成员或局部一说。 静态匿名内部类只能访问外部类的静态属性和方法,非静态匿名内部类可以访问外部类的所有属性和方法,与上述内部类逻辑一致。测试代码如下所示:
//JDK 1.7代码
package innerclass;
public class TestAnonymousClass {
public int publicValue = 0;
public static int publicStaticValue = 0;
private int privateValue = 0;
private static int privateStaticValue = 0;
private Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread");
System.out.println(privateValue);
System.out.println(privateStaticValue);
System.out.println(publicValue);
System.out.println(publicStaticValue);
}
});
private static Thread staticThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("staticThread");
System.out.println(privateStaticValue);
System.out.println(publicStaticValue);
}
});
public static void main(String[] args) {
final int localN = 0;
Thread localThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("localThread");
System.out.println(localN);
System.out.println(privateStaticValue);
System.out.println(publicStaticValue);
}
});
}
private void outF(final int arg1) {
InnerClass innerClass = new InnerClass() {
@Override
void innerF() {
System.out.println(arg1);
System.out.println(privateValue);
System.out.println(privateStaticValue);
System.out.println(publicValue);
System.out.println(publicStaticValue);
outF();
}
};
}
private void outF() {
}
class InnerClass {
void innerF() {
}
}
}
上面展示的demo,编译后将会产生5个内部类字节码文件, 分别对应成员变量thread,静态变量staticThread,内部类InnerClass,main方法中的局部匿名内部类localThread,实例方法outF中的局部匿名内部类innerClass。
private void outF(int arg1) {
InnerClass innerClass = new InnerClass(this, arg1){
final /* synthetic */ int val$arg1;
final /* synthetic */ TestAnonymousClass this$0;
{
this.this$0 = testAnonymousClass;
this.val$arg1 = n;
super(testAnonymousClass);
}
@Override
void innerF() {
System.out.println(this.val$arg1);
System.out.println(TestAnonymousClass.access$000(this.this$0));
System.out.println(TestAnonymousClass.access$100());
System.out.println(this.this$0.publicValue);
System.out.println(publicStaticValue);
TestAnonymousClass.access$200(this.this$0);
}
};
}
我们重点看一下上述匿名内部类的反编译代码。thread和localThread属于非静态的匿名内部类,同样会维护一个外部类类型的成员变量引用外部类的实例。 匿名内部类访问外部类的逻辑同普通内部类一样,直接调用公有属性或方法,调用编译器生成的包级可见域方法间接调用私有属性或方法。
匿名内部类与普通内部类最大的区别在于,匿名内部类可以访问局部变量和形参,在JDK 1.8以前,对局部变量和方法形参的访问,要求该变量和形参加上final修饰符(形参也属于局部变量,以下统称局部变量)。 方法结束之后,整个虚拟机栈包括局部变量在内,都会被销毁,而匿名内部类对象和普通类对象一样,回收的逻辑视垃圾回收器而定。这就会导致当匿名内部类对象还没被回收时,所引用的数据已经被销毁了,这明显是不合理的。 所以Java在编译阶段,就明确要求被匿名内部类访问的局部变量持有final修饰符,同时通过语法糖,将该变量复制成一个成员变量,并通过构造函数传入。这样可以保证即使局部变量被销毁了,匿名内部类对象依然能够访问相等的值。
在JDK 1.8版本中,引入了”effectively final“,只要局部变量在方法体内不做修改,编译器会自动对局部变量声明final。当然,如果对局部变量有赋值语句,则编译报错:
Error:(64, 36) java: 从内部类引用的本地变量必须是最终变量或实际上的最终变量
4.总结
-
内部类字节码命名格式
不管什么类型的内部类,都和普通类并无二致。编译器会在外部类字节码文件相同目录下,为内部类生成字节码文件。 普通内部类的字节码文件命名格式为:OuterClass$InnerClass.class,匿名内部类的命名方式为:OuterClass$index.class,其中index为匿名内部类在源代码中的顺序,如OuterClass$1.class。
-
内部类对外部类的访问
- 静态内部类不能访问外部类的实例属性和方法,可以访问外部类的公有或私有的静态属性或方法。
- 非静态内部类可以访问外部类的实例、静态属性和方法,编译器会为内部类生成一个引用外部类实例的实例属性。
- 静态内部类访问外部类公有属性或方法,直接调用OuterClass.pubValue或者OuterClass.pubFunc即可。访问外部类私有属性或方法,通过调用编译器为外部类生成的包级可见域的方法即可。
- 非静态内部类访问外部类属性或方法的逻辑与第三点类似。
- 除上述逻辑之外,匿名内部类引用的局部变量,都必须是final类型的,JDK 1.8之前要求手动声明final,自JDK 1.8起引入了”effectively final“,不再需要手动声明。
-
附件
本博文使用到的源代码由此获得。
5.参考文献
[1] Lee Benfield.Inner classes have to fake friendship[EB/OL].http://www.benf.org/other/cfr/inner-class-fake-friends.html,2019-06-18.
[2] 浅析java中的语法糖[EB/OL].https://www.cnblogs.com/qingshanli/p/9375040.html,2019-06-18.