【问题标题】:Java determines which method to call using a generic type argument?Java 使用泛型类型参数确定调用哪个方法?
【发布时间】:2013-08-26 23:27:39
【问题描述】:

据我所知,Java 在运行时会丢弃泛型类型参数信息。它仅在编译时用于执行检查,例如,此特定方法调用是否有效。

今天我遇到了以下一段代码,其中,Java 似乎通过集合/列表类型参数来确定要调用哪个构造函数:

public static class MyClass {
    public MyClass(final Collection<String> coll) {
        System.out.println("Constructor 1");
    }
    public MyClass(final List<Integer> list) {
        System.out.println("Constructor 2");
    }
}

进行了以下调用:

new MyClass(new HashSet<String>()); // Constructor 1
new MyClass(new ArrayList<String>()); // Constructor 1
new MyClass(new ArrayList<Integer>()); // Constructor 2

现在,如果我删除类型参数:

public static class MyClass2 {
    public MyClass2(final Collection coll) {
        System.out.println("Constructor 1");
    }
    public MyClass2(final List list) {
        System.out.println("Constructor 2");
    }
}

...和我期望的一样的调用;使用列表参数的构造函数调用适用于“最精确”满足其需求的构造函数:

new MyClass2(new HashSet<String>()); // Constructor 1
new MyClass2(new ArrayList<String>()); // Constructor 2
new MyClass2(new ArrayList<Integer>()); // Constructor 2

似乎泛型信息被存储在编译的类(在本例中为 MyClass)中,毕竟没有被丢弃,但它应该被丢弃。我误会了什么?

【问题讨论】:

  • +1 好问题。我对为什么会这样有一个模糊的想法,但我很高兴看到一个好的答案。
  • 我猜这可能与 Java 选择在编译时使用的方法有关?
  • 在编译时,编译器从一组重载方法中选择“最接近的匹配”。通用参数正在影响编译器用于进行选择的算法。
  • 我将 MyClass 和 MyClass2 分离到一个单独的类中,编译它。然后把main方法放到一个单独的类中,编译并调用。结果是一样的。我想存储了某种通用类型参数信息。

标签: java generics


【解决方案1】:

这里发生的事情是编译器可以通过使用泛型来区分这两个构造函数,所以它创建字节码之前剥离泛型。

在中间情况下,它会告诉 VM 调用 MyClass2&lt;init&gt;(Collection)(即生成与此特定构造函数匹配的字节码)。

VM 不会尝试在运行时确定哪个方法匹配。那太慢了。相反,它依赖于编译器创建非常具体的指令。

这就是为什么即使通用信息在运行时已被删除,上面的代码仍然有效的原因。

[编辑]澄清:字节码包含编译器可以看到和使用的附加信息。您可以通过反射获得相同的信息。

擦除意味着字节码解释器和 JIT 不关心泛型,这就是为什么你不能在同一个类中拥有 setFoo(List&lt;String&gt;)setFoo(List&lt;Integer&gt;):虽然编译器可以区分这两者,但运行时可以' t.

具体来说,当您通过反射检查方法时,您将获得泛型信息,但字节码解释器/JIT 不使用反射。相反,它使用压缩的方法签名,读取类似Method com/pany/Type/setFoo(Ljava.util.List;)V - 这里不再有泛型。

相关:

【讨论】:

  • 我将代码与类和主方法分成两个单独的类。用类编译类,用main方法编译类。使用 main 方法调用类。但结果是一样的。意思是泛型的类型信息存放在第一个类中,然后在编译第二个类的时候用到?
  • 已编译的.class 文件保留其所有的泛型信息。有关您的类定义的通用信息将永远保留在程序中;泛型类型信息仅在那些泛型类型的对象上丢失;例如List&lt;String&gt; 只会知道它是 List
  • @SergejsPogorelovs:编译器和运行时使用不同的信息源。查看我的编辑。
  • 谢谢。我的代码指定了它使用的泛型,并且还在编译的代码中指定了构造函数具有的泛型。所以代码知道它应该调用哪个构造函数。
【解决方案2】:

没有注意到您正在处理构造函数。无论如何,以下参数即使对构造函数也是有效的。


重载方法的方法调用由编译器在编译时解析。泛型仅在编译时用于类型检查。所以,这与类型擦除无关,完全是运行时业务。

考虑您的第一个案例:

public MyClass(final Collection<String> coll)
public MyClass(final List<Integer> list)

现在,当您调用该方法时:

new MyClass(new HashSet<String>()); // Constructor 1
new MyClass(new ArrayList<String>()); // Constructor 1
new MyClass(new ArrayList<Integer>()); // Constructor 2

编译器将决定哪个方法具有更具体的传递参数类型。考虑案例 2,我猜你的主要疑问在哪里。

ArrayList&lt;String&gt;Collection&lt;String&gt; 的子类型,但不是List&lt;Integer&gt; 的子类型。泛型是不变的。因此,编译器会将第二个方法调用绑定到第一个方法。

现在,考虑您使用原始类型的第二种情况:

public MyClass2(final Collection coll)
public MyClass2(final List list)

现在,ArrayList&lt;String&gt;ListCollection 的子类型。但是ListCollection 更具体到ArrayList。所以,编译器会绑定方法调用:

new MyClass2(new ArrayList<String>());

List 作为参数。


参考资料:

【讨论】:

  • 我认为困扰我的是如果通用信息在运行时被丢弃,为什么要在编译代码时存储它,所以它可能在类编译时被丢弃。但是我错了,因为将来编译其他类仍然需要它,这将取决于我们的泛型类。所以我认为通用信息总是存储在字节码中,但在运行时它仍然被丢弃?是否没有某些代码想要调用带有泛型参数的方法,而没有加载详细信息的情况?
【解决方案3】:

简单来说,编译器选择匹配的最窄类型。

在原始参数版本中,很明显会选择什么,但在泛型版本中,集合/列表的类型包含在匹配信息中:一个ArrayList既是集合又是列表,但是你可以看到通过比较 ArrayList 的类型很容易打破平局 - 现在只有一种方法仍然匹配。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-22
    • 1970-01-01
    • 2017-12-01
    • 1970-01-01
    • 2010-12-19
    相关资源
    最近更新 更多