嵌套类的构造函数的问题


这个问题是关于Java的有趣行为:它在某些情况下为嵌套类生成了额外的(非默认的)构造函数。

这个问题也是关于奇怪的匿名类,Java用这个奇怪的构造函数生成。


请考虑以下代码:

package a; import java.lang.reflect.Constructor; public class TestNested { class A { A() { } A(int a) { } } public static void main(String[] args) { Class aClass = A.class; for (Constructor c : aClass.getDeclaredConstructors()) { System.out.println(c); } } } 

这将打印:

 a.TestNested$A(a.TestNested) a.TestNested$A(a.TestNested,int) 

好。 接下来,让构造函数A(int a)私有:

  private A(int a) { } 

再次运行程序。 接收:

 a.TestNested$A(a.TestNested) private a.TestNested$A(a.TestNested,int) 

这也没关系。 但是现在,让我们以这种方式修改main()方法(添加类A创建的新实例):

 public static void main(String[] args) { Class aClass = A.class; for (Constructor c : aClass.getDeclaredConstructors()) { System.out.println(c); } A a = new TestNested().new A(123); // new line of code } 

然后输入变为:

 a.TestNested$A(a.TestNested) private a.TestNested$A(a.TestNested,int) a.TestNested$A(a.TestNested,int,a.TestNested$1) 

它是什么: a.TestNested $ A(a.TestNested,int,a.TestNested $ 1) <<< — ??

好的,让我们再次使构造函数A(int a)包本地化:

  A(int a) { } 

再次重新运行程序(我们不会删除带有A创建实例的行!),输出与第一次一样:

 a.TestNested$A(a.TestNested) a.TestNested$A(a.TestNested,int) 

问题:

1)如何解释这个?

2)这第三个奇怪的构造函数是什么?


更新:调查显示如下。

1)让我们尝试使用来自其他类的reflection来调用这个奇怪的构造函数。 我们无法做到这一点,因为没有办法创建那个奇怪的TestNested$1类的实例。

2)好的。 让我们来做。 让我们向类TestNested添加这样的静态字段:

 public static Object object = new Object() { public void print() { System.out.println("sss"); } }; 

好? 好的,现在我们可以从另一个类调用第三个奇怪的构造函数:

  TestNested tn = new TestNested(); TestNested.A a = (TestNested.A)TestNested.A.class.getDeclaredConstructors()[2].newInstance(tn, 123, TestNested.object); 

对不起,我绝对不明白。


更新-2:进一步的问题是:

3)为什么Java为第三个合成构造函数使用特殊的匿名内部类作为参数类型? 为什么不只是Object类型,具有特殊名称的构造函数?

4) Java可以使用已定义的匿名内部类来实现这些目的吗? 这不是某种违反安全的行为吗?

首先,感谢您提出这个有趣的问题。 我很好奇,以至于无法抗拒看看字节码。 这是TestNested的字节码:

 Compiled from "TestNested.java" public class a.TestNested { public a.TestNested(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc_w #2 // class a/TestNested$A 3: astore_1 4: aload_1 5: invokevirtual #3 // Method java/lang/Class.getDeclaredConstructors:()[Ljava/lang/reflect/Constructor; 8: astore_2 9: aload_2 10: arraylength 11: istore_3 12: iconst_0 13: istore 4 15: iload 4 17: iload_3 18: if_icmpge 41 21: aload_2 22: iload 4 24: aaload 25: astore 5 27: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 30: aload 5 32: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 35: iinc 4, 1 38: goto 15 41: new #2 // class a/TestNested$A 44: dup 45: new #6 // class a/TestNested 48: dup 49: invokespecial #7 // Method "":()V 52: dup 53: invokevirtual #8 // Method java/lang/Object.getClass:()Ljava/lang/Class; 56: pop 57: bipush 123 59: aconst_null 60: invokespecial #9 // Method a/TestNested$A."":(La/TestNested;ILa/TestNested$1;)V 63: astore_2 64: return } 

如您所见,从main方法调用构造函数a.TestNested$A(a.TestNested,int,a.TestNested$1) 。 此外, null作为a.TestNested$1参数的值传递。

那么让我们来看看神秘的匿名类a.TestNested$1

 Compiled from "TestNested.java" class a.TestNested$1 { } 

奇怪 – 我本来希望这个class级能够真正做点什么。 为了理解它,让我们看看a.TestNested$A中的构造函数:a.TestNested $ A {final a.TestNested this $ 0;

  a.TestNested$A(a.TestNested); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field this$0:La/TestNested; 5: aload_0 6: invokespecial #3 // Method java/lang/Object."":()V 9: return private a.TestNested$A(a.TestNested, int); Code: 0: aload_0 1: aload_1 2: putfield #2 // Field this$0:La/TestNested; 5: aload_0 6: invokespecial #3 // Method java/lang/Object."":()V 9: return a.TestNested$A(a.TestNested, int, a.TestNested$1); Code: 0: aload_0 1: aload_1 2: iload_2 3: invokespecial #1 // Method "":(La/TestNested;I)V 6: return } 

查看包可见的构造函数a.TestNested$A(a.TestNested, int, a.TestNested$1) ,我们可以看到第三个参数被忽略。

现在我们可以解释构造函数和匿名内部类。 为了避免私有构造函数的可见性限制,需要额外的构造函数。 这个额外的构造函数只是委托给私有构造函数。 但是,它不能与私有构造函数具有完全相同的签名。 因此,添加匿名内部类以提供唯一的签名,而不会与其他可能的重载构造函数发生冲突,例如带有签名(int,int)(int,Object)的构造函数。 由于这个匿名内部类只需要创建一个唯一的签名,因此不需要实例化它,也不需要有内容。

第三个构造函数是由编译器生成的合成构造函数,以允许从外部类访问私有构造函数。 这是因为内部类(及其封闭类对其私有成员的访问)仅存在于Java语言而不是JVM,因此编译器必须弥合幕后的差距。

反思会告诉你一个成员是否是合成的:

 for (Constructor c : aClass.getDeclaredConstructors()) { System.out.println(c + " " + c.isSynthetic()); } 

这打印:

 a.TestNested$A(a.TestNested) false private a.TestNested$A(a.TestNested,int) false a.TestNested$A(a.TestNested,int,a.TestNested$1) true 

有关进一步的讨论,请参阅此文章: 关于Java中私有静态嵌套类的合成访问器的Eclipse警告?

编辑:有趣的是,eclipse编译器与javac的做法不同。 当使用eclipse时,它会添加一个内部类本身类型的参数:

 a.TestNested$A(a.TestNested) false private a.TestNested$A(a.TestNested,int) false a.TestNested$A(a.TestNested,int,a.TestNested$A) true 

我试图通过提前暴露该构造函数来绊倒它:

 class A { A() { } private A(int a) { } A(int a, A another) { } } 

它通过简单地向合成构造函数添加另一个参数来解决这个问题:

 a.TestNested$A(a.TestNested) false private a.TestNested$A(a.TestNested,int) false a.TestNested$A(a.TestNested,int,a.TestNested$A) false a.TestNested$A(a.TestNested,int,a.TestNested$A,a.TestNested$A) true