【问题标题】:Issue with constructors of nested class嵌套类的构造函数问题
【发布时间】:2012-12-25 07:41:13
【问题描述】:

这个问题是关于 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);

对不起,我完全不明白。


UPDATE-2:其他问题是:

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

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

【问题讨论】:

  • 这是一个匿名的 A 内部类,它被传递给第三个构造函数。问题是它为什么在那里或里面有什么。想反编译吗?
  • @JanDvorak 这是什么原因? Java 可以做得更简单,只需修改构造函数的可访问状态。
  • 但是它可以从外部访问......
  • @Andremoniy 响应您的第二次更新:Java 不能只使用 Object 作为参数类型,因为您可以拥有自己的带有 Object 参数的构造函数并在没有参数的情况下调用它反射(通过使用非私有的A(int, Object) 构造函数编译A,将TestNested$A.class 文件保存在其他位置,删除新的构造函数,重新编译TestNested,并将新的TestNested$A.class 替换为具有@987654346 的版本@构造函数。)使用匿名类型(TestNested$1)作为合成构造函数的参数类型可以防止这种情况发生。
  • @Andremoniy 和关于违反安全性:不可能,因为构造函数的参数类型是在编译时绑定的(当你重新编译外部类时,它也会重新编译内部类)。即使你有一个与匿名类型相同类型的对象,你也不能有一个以该类型作为参数的构造函数,所以编译器会选择一个不同的构造函数来调用。

标签: java reflection nested-class


【解决方案1】:

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

请参阅此帖子以进行进一步讨论:Eclipse warning about synthetic accessor for private static nested classes in Java?

编辑:有趣的是,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

【讨论】:

  • 很好的答案。编译器添加一些综合创建的类并使用它。在反编译的代码中,它只是将 null 写入该参数:new A(123, null)。这样,编译器就可以支持您将没有任何具有该签名的方法。我只是不知道他们为什么没有在这里抛出编译器错误。我们在这里调用内部类的私有方法!?
  • @partlov,太棒了!真的只是null。好的!但是可能有一些方法可以直接从源代码调用这个构造函数,而不是通过反射?
  • @Andremoniy 我认为这不可能。我们在编写代码时没有那个“类”。该类仅在编译后存在。所以,我认为只有通过反射才有可能。
  • @Andremoniy 我认为不可能从 Java 源代码调用合成构造函数。在字节码中,构造函数调用是(我认为)invokespecial 字节码指令,它调用特定的构造函数。编译器不会生成调用合成构造函数的字节码。
  • 是的,但你不认为这只是人为创造的情况。也许编译器有这种逻辑:“好的,如果它存在于这里的某个地方(如果例如 Andremoniy 创建它:)),我将添加带有一些匿名类的构造函数,但如果这样不存在,我将创建一个”。 :)
【解决方案2】:

首先,感谢您提出这个有趣的问题。我太感兴趣了,忍不住看了一下字节码。这是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中的构造函数: 类 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) 的构造函数。由于这个匿名内部类只需要创建唯一签名,因此不需要实例化,也不需要有内容。

【讨论】:

  • 非常有趣的字节码研究。非常感谢!
猜你喜欢
  • 2021-09-08
  • 1970-01-01
  • 1970-01-01
  • 2015-07-03
  • 1970-01-01
  • 1970-01-01
  • 2012-07-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多