Scala:为什么扩展特征会破坏 `Class.getConstructor()`?

Scala: Why does extending a trait break `Class.getConstructor()`?

当 class 扩展特征时,调用 getConstructor() 否则会成功然后抛出 NoSuchMethodException。为什么会发生这种情况,可以采取什么措施?

在下面的代码中,第一次调用 getConstructor() returns 没有错误。第二个引发异常。

trait MyTrait

class MyClass(root: String)

object Main extends App {

  val one = new MyClass("foo")
  one.getClass.getConstructor(classOf[String])
  println("so far so good")

  val two = new MyClass("foo") with MyTrait
  two.getClass.getConstructor(classOf[String]) // NoSuchMethodException

}
new MyClass("foo") with MyTrait

创建一个带有零参数构造函数的新匿名 class(此构造函数在内部执行 super("foo")),然后创建该新 class.

的实例
classOf[MyClass] eq one.getClass
classOf[MyClass] ne two.getClass

由于新的匿名 class 中没有基于 String 的构造函数,您的代码失败了。

让我们仔细看看会发生什么。

我稍微更改了您的定义以简化生成的输出。我的 Main 对象如下所示:

object App {
  def main(args: Array[String]): Unit = {
    val one = new MyClass("foo")
    one.getClass.getConstructor(classOf[String])
    println("so far so good")

    val two = new MyClass("foo") with MyTrait
    two.getClass.getConstructor(classOf[String]) // NoSuchMethodException
  }
}

编译后我们得到以下classes: App$$anon.class, App$.class, App.class, MyClass.class, MyTrait.class.

MyClass.classMyTrait.class 是微不足道的,我不会讨论它们。

让我们看看有趣的 classes:

  1. App.class

这是 javap -c 输出:

public final class test.App {
  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #16                 // Field test/App$.MODULE$:Ltest/App$;
       3: aload_0
       4: invokevirtual #18                 // Method test/App$.main:([Ljava/lang/String;)V
       7: return
}

如我们所见,它只是调用了 App$.main 方法。

  1. App$.class

这是里面的内容:

public final class test.App$ {
  public static test.App$ MODULE$;

  public static {};
    Code:
       0: new           #2                  // class test/App$
       3: invokespecial #14                 // Method "<init>":()V
       6: return

  public void main(java.lang.String[]);
    Code:
       0: new           #19                 // class test/MyClass
       3: dup
       4: ldc           #21                 // String foo
       6: invokespecial #24                 // Method test/MyClass."<init>":(Ljava/lang/String;)V
       9: astore_2
      10: aload_2
      11: invokevirtual #28                 // Method test/MyClass.getClass:()Ljava/lang/Class;
      14: iconst_1
      15: anewarray     #30                 // class java/lang/Class
      18: dup
      19: iconst_0
      20: ldc           #32                 // class java/lang/String
      22: aastore
      23: invokevirtual #36                 // Method java/lang/Class.getConstructor:([Ljava/lang/Class;)Ljava/lang/reflect/Constructor;
      26: pop
      27: getstatic     #41                 // Field scala/Predef$.MODULE$:Lscala/Predef$;
      30: ldc           #43                 // String so far so good
      32: invokevirtual #47                 // Method scala/Predef$.println:(Ljava/lang/Object;)V
      35: new           #7                  // class test/App$$anon
      38: dup
      39: invokespecial #48                 // Method test/App$$anon."<init>":()V
      42: astore_3
      43: aload_3
      44: invokevirtual #28                 // Method test/MyClass.getClass:()Ljava/lang/Class;
      47: iconst_1
      48: anewarray     #30                 // class java/lang/Class
      51: dup
      52: iconst_0
      53: ldc           #32                 // class java/lang/String
      55: aastore
      56: invokevirtual #36                 // Method java/lang/Class.getConstructor:([Ljava/lang/Class;)Ljava/lang/reflect/Constructor;
      59: pop
      60: return

  private test.App$();
    Code:
       0: aload_0
       1: invokespecial #54                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: putstatic     #56                 // Field MODULE$:Ltest/App$;
       8: return
}

最有趣的部分是第 35 和 39 行。

第 35 行告诉我们,实际上我们创建了 App$$anon class 的一个实例。它是一个自动生成的 class 代表 MyClass("foo") with MyTrait.

第 39 行告诉我们如何初始化它。我们调用 App$$anon."<init>":()V 构造函数,但不向其传递任何值。现在我很好奇 "foo" 常量在哪里?

  1. App$$anon.class

这是里面的内容:

public final class test.App$$anon extends test.MyClass implements test.MyTrait {
  public test.App$$anon();
    Code:
       0: aload_0
       1: ldc           #16                 // String foo
       3: invokespecial #19                 // Method test/MyClass."<init>":(Ljava/lang/String;)V
       6: return
}

在这里我们终于看到了我们的 "foo"(第 1 行)。 ldc表示"load constant"; “#16”是对 table 中那个常量的引用;评论说它的类型是 String 并且值是 foo.

第 3 行是对 MyClass."<init>":(Ljava/lang/String;)V 的调用。

TLDR 优化器在自动生成的 class 的构造函数中隐藏常量。所以构造函数本身没有任何参数。

PS:如果我们这样修改代码:

val foo = "foo"
val two = new MyClass(foo) with MyTrait

然后我们得到一个自动生成的 class 和一个接受 String 参数的构造函数:

public final class test.App$$anon extends test.MyClass implements test.MyTrait {
  public test.App$$anon(java.lang.String);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #17                 // Method test/MyClass."<init>":(Ljava/lang/String;)V
       5: return
}