如果我们应用类型擦除,哪些重载方法将在运行时被调用,为啥?

Posted

技术标签:

【中文标题】如果我们应用类型擦除,哪些重载方法将在运行时被调用,为啥?【英文标题】:Which of the overloaded methods will be called on runtime if we apply type erasure, and why?如果我们应用类型擦除,哪些重载方法将在运行时被调用,为什么? 【发布时间】:2016-03-22 05:44:47 【问题描述】:

假设我们有以下泛型类

public class SomeType<T> 
    public <E> void test(Collection<E> collection)
        System.out.println("1st method");

        for (E e : collection)
            System.out.println(e);
        
    

    public void test(List<Integer> integerList)
        System.out.println("2nd method");

        for (Integer integer : integerList)
            System.out.println(integer);
        
    


现在在 main 方法中,我们有以下代码 sn-p

SomeType someType = new SomeType();
List<String> list = Arrays.asList("value");
someType.test(list);

作为执行someType.test(list) 的结果,我们将在控制台中获得“第二个方法”以及java.lang.ClassCastException。据我了解,执行第二个test 方法的原因是我们不对SomeType 使用泛型。因此,编译器会立即从类中删除所有泛型信息(即&lt;T&gt;&lt;E&gt;)。完成第二个test 方法后,List integerList 将作为参数,当然ListList 的匹配比Collection 更好。

现在考虑在 main 方法中我们有以下代码 sn-p

SomeType<?> someType = new SomeType<>();
List<String> list = Arrays.asList("value");
someType.test(list);

在这种情况下,我们将在控制台中获得“第一种方法”。这意味着正在执行的第一个测试方法。问题是为什么?

根据我对运行时的理解,由于类型擦除,我们从来没有任何泛型信息。那么,为什么第二个test 方法无法执行。对我来说,第二个 test 方法应该(在运行时)采用以下形式 public void test(List&lt;Integer&gt; integerList)... 不是吗?

【问题讨论】:

我们在运行时没有泛型信息,但方法选择不是在运行时进行的。 好的,但是方法选择是怎么做的呢?字节码中是否有任何特定信息告诉 jvm 调用哪个方法? @ruvinbsu 因为编译器确实决定了它应该调用哪个方法,是的。 【参考方案1】:

适用的方法匹配之前类型擦除(see JSL 15.12.2.3)。 (擦除意味着运行时类型没有参数化,但方法是在编译时选择的,此时类型参数可用)

list 的类型是List&lt;String&gt;,因此:

test(Collection&lt;E&gt;) 适用,因为List&lt;Integer&gt;Collection&lt;E&gt; 兼容,其中EInteger(形式上,约束公式List&lt;Integer&gt; → Collection&lt;E&gt; [E:=Integer] 简化为true,因为List&lt;Integer&gt;Collection&lt;Integer&gt; 的子类型。

test(List&lt;String&gt;) 不适用,因为List&lt;String&gt;List&lt;Integer&gt; 不兼容(形式上,约束公式List&lt;String&gt;List&lt;Integer&gt; 简化为false,因为String 不是@987654341 的超类型@)。

详情解释隐藏在JSL 18.5.1中。

对于test(Collection&lt;E&gt;)

设 θ 为替换 [E:=Integer]

[...]

一组约束公式C构造如下:令F1,...,Fn为m的形式参数类型,令e1,...,ek为调用的实际参数表达式。

在这种情况下,我们有 F1 = Collection&lt;E&gt;e1 = List&lt;Integer&gt;

那么:【约束公式集】包括‹ei → Fi θ›

在这种情况下,我们有 List&lt;Integer&gt; → Collection&lt;E&gt; [E:=Integer](其中 → 表示在推断出类型变量 E 后 e1 与 F1 兼容)

对于test(List&lt;String&gt;),没有替换(因为没有推理变量),约束只是List&lt;String&gt;List&lt;Integer&gt;

【讨论】:

【参考方案2】:

The JLS is a bit of a rat's nest on this one,但是您可以使用一个非正式的(他们的话,不是我的话)规则:

如果第一个方法处理的任何调用都可以传递给另一个方法而不会出现编译时错误,那么[O]一个方法比另一个方法更具体。

为了论证,我们调用&lt;E&gt; test(Collection&lt;E&gt;)方法1和test(List&lt;Integer&gt;)方法2。

让我们在这里扔一个扳手 - 我们知道整个类是通用的,所以在没有 some 类型的情况下实例化它会产生... 不太理想在运行时进行类型检查。

另一个原因是ListCollection 更具体,如果一个方法被传递给List,它会比Collection 更容易地适应它,需要在编译时检查类型的警告。由于它不是使用那个原始类型,我相信这个特定的检查被跳过了,Java 将List&lt;Integer&gt; 视为Collection&lt;capture(String)&gt; 更具体

您应该向 JVM 提交错误,因为这似乎是不一致的。或者,至少,让编写 JLS 的人用比他们古怪的数学符号略好的英语解释为什么这是合法的......

继续前进;在您的第二个示例中,您礼貌地将您的实例输入为通配符,这允许 Java 做出正确的编译时断言,即 test(Collection&lt;E&gt;) 是可供选择的最安全方法。

请注意,这些检查都不会在运行时发生。这些都是在 Java 运行之前做出的决定,因为模棱两可的方法调用或对带有不受支持参数的方法的调用会导致 编译时 错误。

故事的寓意:不要使用原始类型。它们是邪恶的。它使类型系统以奇怪的方式运行,它实际上只是为了保持向后兼容性。

【讨论】:

我现在理解的是编译器不仅检查错误/错误并生成字节码。它还向 jvm 提供了一些关于将来调用哪个方法的具体信息。因此,这意味着编译器不是简单地直接翻译源代码而是为未来做出决策的人。

以上是关于如果我们应用类型擦除,哪些重载方法将在运行时被调用,为啥?的主要内容,如果未能解决你的问题,请参考以下文章

为啥“避免方法重载”?

JVM是如何进行方法调用的

java学习笔记9.22(泛型)

重载和重写

UITableViewDelegate dealloc 方法在应用程序仍在运行时被调用

JAVA构造器,重载与重写