我们来看一坨代码:
public class A{
public static void main(String[] args){
B b = new A.B();
}
private static class B{
}
}
上述代码,如果我们执行javac A.java
编译,会产生几个class文件?
我们分析下:
- A.class 肯定有
- 有个静态内部类B,还有个A$B.class
两个。
实际运行你会发现有三个:
- A.class
- A$B.class
- A$1.class
问题来了:
- 为什么会多了个A$1.class?
- Java里面
java.lang.reflect.Method
以及java.lang.Class
都有一个isSynthetic()
方法,是什么意思?
更多问答 >>
-
2020-08-26 21:11
-
2020-09-09 23:54
-
2020-10-03 11:43
-
每日一问 | 启动了Activity 的 app 至少有几个线程?
2020-10-12 00:47 -
每日一问 | 玩转 Gradle,可不能不熟悉 Transform,那么,我要开始问了。
2020-10-26 23:45 -
每日一问 | apply plugin: 'com.android.application' 背后发生了什么?
2020-08-16 19:56 -
每日一问| View 绘制的一个细节,如何修改 View 绘制的顺序?
2020-08-12 10:21 -
每日一问 | 比 removeView 更轻量的操作,你了解过吗?
2020-07-27 01:14 -
每日一问 | RecyclerView的多级缓存机制,每级缓存到底起到什么样的作用?
2020-07-19 23:56 -
2020-07-08 23:05
哈哈哈哈哈哈哈,为什么会多了个
其实就是为了实例化class B。是这样的:大家都知道,在java文件中编写的内部类,在编译成class之后,都会像是被抽出来一个单独的类一样,也就是从内部类变成了普通的类。因为内部类B没有定义构造方法,在编译时编译器就会自动帮它生成一个,访问权限都是跟随这个类的访问权限的,比如:A$1.class
呢?public class B,生成的构造方法的访问权限就是public;
protected class B ——> protected B() {};
class B ——> B() {};
private class B ——> private B() {};
所以反编译内部类B,会看到它的构造方法是private的:
那么问题来了:
编译成class之后,内部类会以普通类的方式存在,而且构造方法又是private,那怎么能在A中实例化它呢?其实,我们对这种场景已经无比熟悉了,哈哈哈哈没错就是单例了!在一个单例的类里面,通常会用private来修饰构造方法,这样的话就能避免在外部直接调用。此外,还会有一个可供外部访问的getInstance
方法,用来获取内部实例化好了的对象。而编译器在编译时也是用到了这种思想:既然原来的private构造方法无法改变,那我还可以新增一个可以让外部访问的构造方法啊!不过,现在已经有一个无参构造方法了,再加一个的话,怎么保证不会跟现有的构造方法签名(参数类型)冲突呢?于是,内部类A$1就诞生了!用这个类当参数,可以保证签名不会冲突,你总不能声明一个类名为1的内部类吧?可能有同学跟我想的一样:如果我这样写:再编译,会发生什么呢?
哈哈哈哈哈哈哈:编译器居然死掉了(报NullPointerException了)。。。。。
好啦,把那个class A$1删掉,在
main
方法中看看B有几个构造方法:运行一下:
看吧,多了个参数为A$1的构造方法,证实了前面的说法是对的。
Class以及Member(Constructor、Method、Field也是直接或间接继承自这个类,所以它们三个同样会有这个方法)中的
先看一下它们各自的实现:首先是Class:isSynthetic
方法,是什么来的?再到Executable(因为Executable的子类Constructor和Method都没有重写这个方法,所以看这一个类就行了):
最后是Field:
emmmm,可以看出来,它们各自实现的
这个标识是怎么来的呢?从JDK源码里找不到的,只能去看字节码了:先回想一下,刚刚的A$1这个类,以及内部类B中的isSynthetic
方法,都有一个共同点就是:最后都是检查accessFlags
有没有SYNTHETIC标识。A$B(A$1) {}
这个构造方法,他们都是编译器自动帮我们生成的,我们在代码中既不能直接看到,也不能通过常规方式来使用,而SYNTHETIC有人工合成的意思。熟悉javap的同学会知道它有一个 -v 选项,可以查看类、方法、变量的flags,用这个选项来分别看下A$1和A$B的信息:javap -v A\$1.class :咦,看class A$1下面,那个flags刚好有ACC_SYNTHETIC字眼!
再看内部类B:javap -v A\$B.class :嘿嘿!在构造方法A$B(A$1)下面的flags同样也有个ACC_SYNTHETIC!
先别急着出结论,我们再来修改一下代码:很简单,就在B中加个私有变量i,然后在
重新编译后再次 javap -v A\$B.class :main
中赋值并打印。有没有看到,除了刚刚的构造方法A$B(A$1)之外,还多了两个静态方法
且来看一下A的access$102(A$B, int)
和access$100(A$B)
,而这两个方法下面也是有ACC_SYNTHETIC字眼!main
方法:javap -v A.class :!!在A中操作的i,居然换成了通过调用静态方法的方式来访问!
思考一下:因为B在编译后已经变成了普通的类,而B里面的变量i
是用private修饰的,按照正常的逻辑,私有变量肯定不能直接访问,所以编译器在编译的时候,会给i
生成静态的setter
和getter
方法。我们在A中操作的i
,在编译时自然也就替换成通过静态方法的方式来访问了。最后还有一种情况,就是非静态内部类。
这个大家都能说出来:非静态内部类持有外部类的……修改下代码,把内部类B改成非静态的:看下字节码 javap -v A\$B.class :
好了,来总结一下:
因java的内部类在编译后会变成普通的类,而产生一个问题:既然变成了普通类,那么外部类如果要访问这个内部类的私有成员or方法,怎么办呢?编译器帮我们解决了这个问题:要是外部类访问内部类的成员变量是私有的,那编译器就会额外生成可供外部类访问并且不会跟现有方法冲突的
setter
和getter
;如果访问的是私有的方法,也是同样做法:额外生成可供外部类访问的包装方法;
若构造方法用private修饰了,外部类要访问的话,则会自动创建一个类,并用这个类当作新构造方法的参数来生成对应实例(如果这个类的类名跟已声明的类有冲突,编译器就会死掉);
编译器在添加这些额外的方法时,还会给它们加入一个ACC_SYNTHETIC flag,以便在程序中与普通方法区分开来。
根据上面这一段话可以想到:ACC_SYNTHETIC就是编译器为了能让我们通过常规方式访问到本来访问不到的构造方法、成员变量、成员方法而做的额外事情的标记。
tql
点赞👍
问一下,在 Windows 上,需要去执行 javap -v 'A$1.class'(使用单引号引起来),这是为什么?
应该是含有$特殊符号的原因
1:首先,在jvm里是没有内部类这个概念的,所以编译器在处理内部类的时候都会生成一个新的class文件,那么为什么会额外生成一个A$1.class呢?
因为B类的访问修饰符是private,而其构造函数默认是private的(代码里没有写,但是编译器会自动生成)。private的构造函数就导致了A类无法直接访问B类,那怎么办呢?这个时候编译器就会给B类创建一个新的构造函数,而重载一个新的构造函数必须得设置新的参数,所以这里新建了一个 A$1来当做参数这里我们可以使用 javap -private A$B.class 来简单查看
再看看 A$1 到底是个啥 javap -c A$1.class
很简单,甚至连构造函数都没有,这个就是所谓的合成类(synthetic class),由编译器自行生成,常用于各种内部类。
而我们在A类里使用 new A.B() ,其实调用的就是 B类里的 A$B(A$1),可以使用 javap -c A.class 来看看A类里到底干了啥
Method A$B."<init>":(LA$1;)V 从这个方法注释就能很清晰的看出来调用的是 A$B(A$1) 构造方法,aconst_null代表其参数就是一个null
大佬
发现一件很有趣的事:
之前的回答说到,如果手动创建一个名字跟【某个静态内部类】一样的类,比如这里的TestInner:在编译成class之后,TestInner会变成Test$TestInner。
但如果手动创建一个名为Test$TestInner的Java类,在编译的时候编译器会出错(是编译器内部出错,不是代码问题),但是!!!如果这个类是用Kotlin写的话:用Kotlin编译器却能成功编译!
这意味着什么?也就是对于一些第三方的类库,如果想要修改某个静态内部类的行为而又无从通过继承或反射解决时,可以在同级包名下,重新声明这个 “静态内部类” !在编译打包过程中,IDE会将原来的静态内部类替换成我们自己写的这个类!我已经测试过了。这个算是发现新大陆了~
那个Layout Editor,依赖了IDEA本身的各种Manger,但如果直接调用【init IDEA 工作环境时】创建的Manger,是会报ClassCastException的,因为所属Clas ...查看更多
那个Layout Editor,依赖了IDEA本身的各种Manger,但如果直接调用【init IDEA 工作环境时】创建的Manger,是会报ClassCastException的,因为所属ClassLoader不同,只能通过修改对应Manger的getInstance方法来解决。。。于是无意中就发现了这个,哈哈
骚操作啊
小缘的回答已经很好了,但是大家也能够想到,这种处理实际上非常的不好:
针对这些问题,虚拟机团队也就出了解决方案,在java11里添加了嵌套访问控制,具体而言如下:
这是使用java14编译出来的字节码,可以看到现在的字节码与我们认知的一样了。
但是有个问题就是,这是三个独立的类,A是如何做到访问另一个类的私有方法以及私有属性的呢?难道访问权限规则变了?
还真是变了。。
注意到如下几个属性:
相信不用过多的解释,仅仅从字面上就能猜到这是干嘛的。就是说两个类有相同的爸爸或者就是父子关系,那么就能访问私有的,JVM增加了这个规则之后,字节码看起来就和我们理解的一致了。