【问题标题】:Why does the Java 8 generic type inference pick this overload?为什么 Java 8 泛型类型推断会选择这种重载?
【发布时间】:2015-08-11 20:54:35
【问题描述】:

考虑以下程序:

public class GenericTypeInference {

    public static void main(String[] args) {
        print(new SillyGenericWrapper().get());
    }

    private static void print(Object object) {
        System.out.println("Object");
    }

    private static void print(String string) {
        System.out.println("String");
    }

    public static class SillyGenericWrapper {
        public <T> T get() {
            return null;
        }
    }
}

它在 Java 8 下打印“String”,在 Java 7 下打印“Object”。

我原以为这在 Java 8 中会产生歧义,因为这两个重载方法都匹配。为什么编译器在JEP 101 之后选择print(String)

无论是否合理,这都会破坏向后兼容性,并且在编译时无法检测到更改。升级到 Java 8 后,代码只是偷偷摸摸地表现不同。

注意:SillyGenericWrapper 被命名为“傻”是有原因的。我试图理解为什么编译器的行为方式是这样,首先不要告诉我愚蠢的包装器是一个糟糕的设计。

更新:我还尝试在 Java 8 下编译和运行该示例,但使用的是 Java 7 语言级别。该行为与 Java 7 一致。这是意料之中的,但我仍然觉得需要验证。

【问题讨论】:

  • 我用 jdk 1.8.0 或 jdk1.8.45 尝试了 eclipse,它打印出 Object
  • 我在 Ideone.com 上获得了字符串(使用 sun-jdk-8u25)。
  • 我会在问题标题和正文中将覆盖更改为重载。不涉及覆盖。
  • @Giovanni:我想我们应该在未来几年禁止 Java 8 相关问题中的任何 Eclipse 特定声明......(不要误会我的意思,我真的希望将 Eclipse 与 Java 8 一起使用)
  • 我在 Oracle jdk1.8.0_45 上获得了 String

标签: java generics java-8 language-lawyer


【解决方案1】:

Java 8 对类型推断规则进行了重大改革;最值得注意的是,目标类型推断得到了很大改进。因此,虽然在 Java 8 之前,方法参数站点没有收到任何推断,默认为 Object,但在 Java 8 中推断出最具体的适用类型,在本例中为 String。 Java 8 的 JLS 引入了一个新的章节 Chapter 18. Type Inference,这在 Java 7 的 JLS 中没有。

JDK 1.8 的早期版本(直到 1.8.0_25)在编译器成功编译代码时存在与重载方法解析相关的错误,根据 JLS 应该产生歧义错误 Why is this method overloading ambiguous? 正如 Marco13 在 cmets 中指出的那样

JLS 的这一部分可能是最复杂的部分

它解释了 JDK 1.8 早期版本中的错误以及您看到的兼容性问题。


如 Java 教程 (Type Inference) 中的示例所示

考虑以下方法:

void processStringList(List<String> stringList) {
    // process stringList
}

假设您想使用一个空列表调用方法 processStringList。在 Java SE 7 中,以下语句无法编译:

processStringList(Collections.emptyList());

Java SE 7 编译器生成类似于以下内容的错误消息:

List<Object> cannot be converted to List<String>

编译器需要类型参数 T 的值,因此它以值 Object 开头。因此,Collections.emptyList 的调用返回一个 List 类型的值,它与方法 processStringList 不兼容。因此,在 Java SE 7 中,您必须指定 type 参数的值,如下所示:

processStringList(Collections.<String>emptyList());

这在 Java SE 8 中不再需要。什么是目标类型的概念已扩展为包括方法参数,例如方法 processStringList 的参数。在这种情况下,processStringList 需要 List 类型的参数

Collections.emptyList() 是类似于问题中的get() 方法的通用方法。 在 Java 7 中,print(String string) 方法甚至不适用于方法调用,因此它不参与重载决策过程。而在 Java 8 中,这两种方法都适用。

这种不兼容性值得在Compatibility Guide for JDK 8 中提及。


您可以查看我的回答,了解与重载方法解析相关的类似问题Method overload ambiguity with Java 8 ternary conditional and unboxed primitives

根据JLS 15.12.2.5 Choosing the Most Specific Method

如果多个成员方法既可访问又适用于 方法调用,需要选择一个来提供 运行时方法分派的描述符。 Java 编程 语言使用选择最具体方法的规则。

然后:

一种适用的方法 m1 比另一种适用的方法更具体 方法 m2,用于使用参数表达式 e1、...、ek、if 的调用 以下任何一项都是正确的:

  1. m2 是通用的,m1 被推断为比 m2 更具体 §18.5.4 中的参数表达式 e1, ..., ek。

  2. m2 不是泛型,m1 和 m2 可以严格或宽松地适用 调用,其中 m1 具有形式参数类型 S1、...、Sn 和 m2 有形参类型 T1, ..., Tn,类型 Si 更具体 比所有 i (1 ≤ i ≤ n, n = k) 的参数 ei 的 Ti。

  3. m2 不是通用的,m1 和 m2 可通过变量arity 应用 调用,其中 m1 的前 k 个变量参数类型 是 S1, ..., Sk 和 m2 的前 k 个可变参数类型 是 T1, ..., Tk,对于参数 ei,类型 Si 比 Ti 更具体 对于所有 i (1 ≤ i ≤ k)。另外,如果 m2 有 k+1 个参数,那么 m1 的第 k+1 个变量参数类型是 m2的第k+1个变量arity参数类型。

上述条件是唯一一种方法可能比另一种方法更具体的情况。

如果 S <: t s>

三个选项中的第二个符合我们的情况。因为StringObject (String &lt;: Object) 的子类型,所以它更具体。因此,方法本身更具体。在 JLS 之后,此方法也更具体最具体,由编译器选择。

【讨论】:

  • 感谢您的参考。我仍然不清楚。选择最具体的方法不是 Java 8 中添加的内容。Java 7 的第 15.12.2.5 节有类似的措辞。我们可以将问题改写为“为什么 Java 7 选择 Object 重载?它不应该选择最具体的吗?”
  • @Lii - 嗯?我认为这个答案是正确和详细的。当然,任何足够复杂的答案都无法与“仅仅因为”区分开来。
  • @Lii 你是对的,我没有详细介绍类型推断,尽管我提到类型推断在 Java 8 中进行了重大改革。我更新了我的答案,提供了一个解释的示例在 Java 教程中docs.oracle.com/javase/tutorial/java/generics/…
  • @BogdanCalmac 请查看我的更新答案。我更详细地介绍了类型推断。关键是方法 print(String string) 甚至不适用于 Java 7 中的调用
  • @Lii 你说得对,这是一个多边形表达式。要回答您的问题:编译器首先需要找到所有适用的方法,这两种方法都适用。 print(Object) 适用于推断类型 Object,print(String) 适用于推断类型 String。下一步是编译器找到最具体的方法。从 JLS 来看,这个过程独立于调用上下文,即只有方法参数很重要。
【解决方案2】:

在 java7 中,表达式是自下而上解释的(很少有例外);子表达式的含义是一种“上下文无关”。对于方法调用,参数的类型首先被解析;然后编译器使用该信息来解析调用的含义,例如,在适用的重载方法中选择一个获胜者。

在 java8 中,这种理念不再适用,因为我们希望在任何地方都使用隐式 lambda(如 x-&gt;foo(x));未指定 lambda 参数类型,必须从上下文中推断。这意味着,对于方法调用,有时方法参数类型决定参数类型。

如果方法被重载,显然会有两难选择。因此,在某些情况下,有必要在编译参数之前先解决方法重载问题以选出一个获胜者。

这是一个重大转变;并且像您这样的一些旧代码将成为不兼容的牺牲品。

一种解决方法是通过“转换上下文”为参数提供“目标类型”

    print( (Object)new SillyGenericWrapper().get() );

或者像@Holger 的建议一样,提供类型参数&lt;Object&gt;get() 以避免一起推断。


Java 方法重载极其复杂;复杂性的好处是值得怀疑的。请记住,重载从来不是必需的——如果它们是不同的方法,你可以给它们不同的名字。

【讨论】:

  • @Lii - 谢谢。我懒得详述(而且我大部分都不记得了);这整件事非常神奇。
  • 细节的要点是:Java 7 仅通过使用= 的赋值进行目标类型推断,然后仅作为一种“回退”。此外,类型推断发生在考虑重载之前。 Java 8 将尝试对每个可能的重载进行推理,然后选择最具体的。
  • @Radiodef - 是的。更详细一点,java7 #15.12.2.8 也适用于 return 语句,但规范对此并不十分清楚。 (太迂腐了;我们怎么了?:)
  • @Radiodef - 不,在这种情况下,java8 首先进行重载解析,选择 one 方法;然后使用该方法参数类型进行目标类型。
  • 15.12.2.8 是我所说的“后备”。这就是为什么例如List&lt;Object&gt; x = unmodifiableList(new ArrayList&lt;String&gt;()); 在 Java 7 中失败,因为仅当推断类型未解决时才考虑目标类型。 WRT 到“何时”确切地 Java 8 进行推理,我发现它不清楚,因为两个部分相互引用。重载解决方案需要推理来确定适用性,并且推理需要为目标类型选择重载。我不确定它是否必须定义,但现在看来推理实际上是在重载解决期间发生的。
【解决方案3】:

首先它与覆盖无关,但它必须处理重载。

Jls,. Section 15 提供了大量关于编译器如何准确选择重载方法的信息

在编译时选择最具体的方法;它的描述符 确定在运行时实际执行的方法。

所以调用的时候

print(new SillyGenericWrapper().get());

编译器选择String 版本而不是Object,因为采用Stringprint 方法比采用Object 的方法更具体。如果有 Integer 而不是 String 那么它将被选中。

此外,如果您想调用以Object为参数的方法,则可以将返回值分配给object类型的参数,例如

public class GenericTypeInference {

    public static void main(String[] args) {
        final SillyGenericWrapper sillyGenericWrapper = new SillyGenericWrapper();
        final Object o = sillyGenericWrapper.get();
        print(o);
        print(sillyGenericWrapper.get());
    }

    private static void print(Object object) {
        System.out.println("Object");
    }

    private static void print(Integer integer) {
        System.out.println("Integer");
    }

    public static class SillyGenericWrapper {
        public <T> T get() {
            return null;
        }
    }
}

输出

Object
Integer

当假设您有 2 个符合重载条件的有效方法定义时,情况开始变得有趣。例如

private static void print(Integer integer) {
    System.out.println("Integer");
}

private static void print(String integer) {
    System.out.println("String");
}

现在如果你调用

print(sillyGenericWrapper.get());

编译器将有 2 个有效的方法定义可供选择,因此您将收到编译错误,因为它不能优先选择一种方法。

【讨论】:

  • 当然,你也可以只用print(new SillyGenericWrapper().&lt;Object&gt;get());调用print(Object)。 +1 指向重载决议。
  • 这个答案没有解决关于如何选择 get 的类型以及为什么这在 Java 8 中不同的问题。它链接到 Java 7 的 JLS 并将其用作Java 8 中的行为,即使 Java 7 中的行为不同。
  • 没有注意到我粘贴了指向 Java-7 而不是 Java-8 jls 的链接。已更正。你能解释一下什么不是更清楚
  • 由于您引用的 JLS 中的部分对于 Java 7 和 8 是相同的,因此无法清楚地解释为什么两个版本的行为不同。您不知何故从本节得出错误的结论。否则,该行为也将是您在 Java 7 中描述的行为,但事实并非如此。
【解决方案4】:

我使用 Java 1.8.0_40 运行它并得到“对象”。

如果您将运行以下代码:

public class GenericTypeInference {

private static final String fmt = "%24s: %s%n";
public static void main(String[] args) {

    print(new SillyGenericWrapper().get());

    Method[] allMethods = SillyGenericWrapper.class.getDeclaredMethods();
    for (Method m : allMethods) {
        System.out.format("%s%n", m.toGenericString());
        System.out.format(fmt, "ReturnType", m.getReturnType());
        System.out.format(fmt, "GenericReturnType", m.getGenericReturnType());   
   }

   private static void print(Object object) {
       System.out.println("Object");
   }

   private static void print(String string) {
       System.out.println("String");
   }

   public static class SillyGenericWrapper {
       public <T> T get() {
           return null;
       }
   }
}

你会看到你得到:

对象公共 T com.xxx.GenericTypeInference$SillyGenericWrapper.get() ReturnType:类 java.lang.Object 通用返回类型:T

这解释了为什么使用 Object 重载的方法而不是 String 的方法。

【讨论】:

  • 这不是转移问题吗?选择对象覆盖是因为 T 被推断为对象。但现在的问题是“为什么将 T 推断为 Object?”?
  • 您是否使用 Java 8 编译它?因为这就是与众不同的原因。
  • 这与问题无关。方法的返回类型的erasure,也就是getReturnType返回的,显然是Object
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-09-16
  • 1970-01-01
相关资源
最近更新 更多