嵌套类构造函数出现问题

15

这个问题涉及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<A> 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<A> 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) 让我们尝试从其他类中使用反射调用这个奇怪的构造函数。 我们将无法做到这一点,因为没有任何方法可以创建那个奇怪的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为什么要用已定义的匿名内部类来实现这些目的?这不是某种安全违规吗?


1
@JanDvorak 这是什么原因呢?Java可以更轻松地实现这个,只需修改构造函数的可访问状态即可。 - Andremoniy
1
但这样它就可以从外部访问了... - John Dvorak
@JanDvorak,同意。好的。那么这第三个内部类a.TestNested$1是什么?通过反射调查,这个类甚至没有任何构造函数,并且不能使用反射实例化。 - Andremoniy
1
针对您的第二个更新:Java不能仅使用Object作为参数类型,因为您可以拥有一个带有Object参数的构造函数并在没有反射的情况下调用它(通过编译具有非私有A(int,Object)构造函数的A,将TestNested$A.class文件保存在其他地方,删除新构造函数,重新编译TestNested,并用具有A(int,Object)构造函数的版本替换新的TestNested$A.class)。使用匿名类型(TestNested$1)作为合成构造函数的参数类型可以防止这种情况的发生。 - matts
1
@Andremoniy 关于违反安全性的问题:不可能,因为构造函数的参数类型在编译时已经绑定(当重新编译外部类时,内部类也会被重新编译)。即使您有一个与匿名类型相同类型的对象,您也不能拥有以该类型作为参数的构造函数,因此编译器将选择调用其他构造函数。 - matts
显示剩余11条评论
2个回答

9

第三个构造函数是编译器生成的一个合成构造函数,以便允许外部类访问私有构造函数。这是因为内部类(及其封装类对其私有成员的访问)仅存在于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

1
非常好的答案。编译器会添加一些合成类并使用它。在反编译的代码中,它只是将 null 写入该参数:new A(123, null)。以此方式,编译器确保您不会有任何具有该签名的方法。我只是不知道为什么他们没有在这里抛出编译器错误。我们在这里调用内部类的私有方法!? - partlov
1
@partlov,太好了!它真的只是null。好的!但也许有一些方法可以直接从源代码中调用这个构造函数,而不是通过反射? - Andremoniy
1
@Andremoniy 我认为这是不可能的。我们在编写代码时没有那个“类”。那个类只存在于编译之后。因此,我认为只有通过反射才可能实现。 - partlov
1
是啊,但你不觉得这只是人为制造的情况吗?也许编译器有这样的逻辑:“好的,如果某个匿名类存在于这里(例如Andremoniy创建了它:)),那么我将添加带有构造函数的类,但如果不存在,我将创建一个。” :) - partlov
1
@Andremoniy 请查看此帖子 - Paul Bellora
显示剩余5条评论

8

首先,感谢您提出这个有趣的问题。我很好奇,所以不能抵制查看字节码的诱惑。这是TestNested的字节码:

Compiled from "TestNested.java"
  public class a.TestNested {
    public a.TestNested();
      Code:
         0: aload_0       
         1: invokespecial #1                  // Method java/lang/Object."<init>":()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 "<init>":()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."<init>":(La/TestNested;ILa/TestNested$1;)V
        63: astore_2      
        64: return        
  }

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

因此,让我们来看看神秘的匿名类a.TestNested$1

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

奇怪,我本来期望这个类有所作为。为了理解它,让我们来看一下a.TestNested$A中的构造函数: class 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."<init>":()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."<init>":()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 "<init>":(La/TestNested;I)V
       6: return        
}

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

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


2
非常有趣的字节码调查。非常感谢你! - Andremoniy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接