一个源文件怎么会生成这么多的.class
文件呢?下面通过问答的形式阐述Java在编译内部类的私有构造函数时采用的策略。JDK版本为1.8.0_111
Q: 为什么会生成 Outer$Inner ?
因为Inner
是Outer
的内部类,而在JVM层面没有内部类的概念,每个类都要有与之对应的.class
文件。所以在源码中Outer.Inner
类在编译之后对应的文件就是Outer$Inner.class
Q: 那为什么要生成 Outer$1 ?
额。。。这个不太清楚额。要不咱们来看看Outer$1.class
的文件内容?
Q: 为毛Outer$1 里面啥都没有啊?
不应该啊,怎么连默认构造函数都没有?真是个神奇的存在,你看,我写了一个Empty
类,编译之后不是空的啊,至少还有个默认的构造函数!!
Q: 所以Outer$1 这个空类到底是干嘛用的?
我也不知道啊,编译器怎么生成了这么个空空荡荡的类,连构造函数都没有,不能实例化啊,要它有什么用。这个类肯定在Outer
或者是Outer$Inner
中被用到了。
长这么漂亮一定是与内部类有关,咱们先来看看Outer$Inner.class
中有没有用到它吧。
快看快看,Outer$Inner
类有两个构造函数,第二个构造函数用到Outer$1
了
Q: 等等,为什么这两个构造函数源码里面都没写啊?
废话!源码里面还没有定义Outer$1
这个类呢,这些都是编译器加的啊。
这两个构造函数的第一个参数都是Outer
类型,由于在Java语言层面,Inner
类可以访问Outer
类所有的成员,而JVM是没有内部类的概念的,所以在初始化Inner
类时,要将Outer
类的对象传入,这样在Inner
类中就可以访问Outer
类中的成员了。final Outer this$0
就是用来持有Outer
类对象的引用的。
第一个构造函数private Outer$Inner(Outer)
对应的就是源码里面的那个无参构造函数。
Q: 那么为什么会出现第二个构造函数呢?
你有没有发现,Outer.Inner
的构造函数是private
的,这样一来,就不能在Outer
中实例化Outer.Inner
类了,不能调用它的构造函数啊(JVM遵循与Java相同的访问控制)!但是Java语言又允许这样做,你看,在Outer.foo()
函数中不就实例化了么,编译还通过了。所以编译器一定要做些微小的工作,来保证能实例化成功。
为了能在Outer
类中成功实例化Outer$Inner
,编译器给Outer$Inner
加了一个Outer
类可以访问的构造函数,在该构造函数内部调用那个无法访问的构造函数,进行初始化工作(第2行,invokespecial #1)。
Q: 为毛这么麻烦啊,直接提升默认构造函数的可见性不就行了?
你啊,还是Naive,提升可见性之后不就与源码中的private
语义冲突了么?其它类本来不能实例化Outer$Inner
的,结果被你一提升,其它类也能实例化了。
我们来举个例子吧。假设咱们编译时把private
提升为package-private
,那么相当于在源码里面删掉了private
,编译之后就与Outer.Inner2
一模一样
这样本来不能被其它类实例化的就可以被实例化了!
看,Outer.Inner2
就相当于被提升可见性的Outer.Inner
,Outer.Inner2
被成功实例化,而本来Outer.Inner
是不能被实例化的。
Q: 好好好,你说的都对。那就加一个构造函数咯,话说第二个参数Outer$1不是空空如也,不能被实例化么?那怎么传进去呢?
谁说传参一定要传实例化后的对象,可以直接传null啊,来来来,咱们看看Outer.class
的内容,看看它是怎么传参的
仔细看看foo
函数,第4行aload_0
传第一个参数,第5行aconst_null
传第二个参数null
[jvms-6.5]。所以第二个参数是以null
传进去的,并不需要实例化Outer$1
对象。
Q: 辛辛苦苦弄一个Outer$1出来,又不实例化,那要它干嘛,直接用Object不行么?反正是传null进去啊,还不用新生成一个类
骚年,你问的很好啊,新生成的Outer$1
好像就是用来指定第二个参数的类型,并没有其它的作用。那么为什么不用已有类来作为第二个参数的类型呢,比如Object,String?看看下面的代码
在源码里面给Outer.Inner
类加一个构造函数,你看看在Outer$Inner.class
里面,编译器为private
构造函数生成的构造函数与Inner(Object o)
对应的构造函数是不是很相似啊?
你把第二个参数用Object试试,把Outer$1
换成java.lang.Object
,那不就有两个签名相同的构造函数了吗?那就冲突了啊!用一个新类型作为第二个参数,就是为了避免与已有构造函数冲突啊!
骚年,是不是豁然开朗!
Q: 说得好像很有道理,问你啊,上面的例子中 new Inner(null) 会调用哪个构造函数呢?那两个相似的构造函数都可以用啊?
当然是Inner(Object o)
啊,尽管编译器给private Inner(){}
加了参数,但是在Java语言层面它还是无参的啊,所以肯定是用有一个参数的构造函数啊!
Q: 所以新生成一个类型Outer$1是为了避免冲突,一般人不会定义这样的类型。但是有人非要弄一个这样的类型作为参数呢?
嘿嘿嘿,聪明的编译器早就想到了,它会拒绝编译!请看下图
Q: 原来如此,那你总结一下吧
编译内部类的私有构造函数时,为了使外部类能实例化内部类,会给内部类添加对应的构造函数(可访问性为default),在对应的私有构造函数参数列表中添加一个类型为Outer$1
的参数,并在所添加的构造函数中调用对应的私有构造函数,进行初始化。举个例子:
构造函数Inner()
、Inner(int i, String s)
会被编译器处理,Inner(int i)
不会,因为在Outer
中没有实例化。
(完)
Reference: