为什么Java 8泛型类型推断选择了这个重载?

时间:2015-05-29 05:45:40

标签: java generics java-8 language-lawyer

考虑以下计划:

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一致。这是预期的,但我仍然觉得需要验证。

4 个答案:

答案 0 :(得分:18)

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

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

  

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

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

如Java Tutoral(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

  

如果多个成员方法都可访问且适用于a   方法调用,有必要选择一个提供   运行时方法调度的描述符。 Java编程   language使用选择最具体方法的规则。

然后:

  

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

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

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

  4.   
  5. m2不是通用的,m1和m2适用于变量arity   调用,以及m1的第一个k变量arity参数类型   是S1,...,Sk和m2的第一个k变量arity参数类型   是T1,...,Tk,Si的类型比参数ei的Ti更具体   对于所有i(1≤i≤k)。另外,如果m2有k + 1个参数,那么   m1的第k + 1个可变arity参数类型是该类型的子类型   k + 1的可变参数类型m2。

  6.         

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

         

    如果S&lt;:T(§4.10),则类型S比任何表达式的类型T更具特异性。

三个选项中的第二个与我们的案例相符。由于StringObjectString <: Object)的子类型,因此更具体。因此,该方法本身更具体。遵循JLS,此方法也严格更具体最具体,并由编译器选择。

答案 1 :(得分:6)

在java7中,表达式是自下而上解释的(极少数例外);子表达式的含义是&#34;无上下文&#34;。对于方法调用,参数的类型是拳头解析;然后,编译器使用该信息来解析调用的含义,例如,在适用的重载方法中选择一个胜利者。

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

如果方法过载,显然存在进退两难的问题。因此,在某些情况下,在编译参数之前,首先要解决方法重载以选择一个获胜者是必要的。

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

解决方法是提供&#34;目标类型&#34;使用&#34;铸造上下文&#34;

的参数
    print( (Object)new SillyGenericWrapper().get() );

或者像@ Holger的建议,提供类型参数<Object>get()以避免一起推断。

Java方法重载非常复杂;复杂性的好处是可疑的。请记住,重载永远不是必需的 - 如果它们是不同的方法,您可以给它们不同的名称。

答案 2 :(得分:2)

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

JLS ,. Section 15提供了有关编译器如何选择重载方法

的大量信息
  

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

所以在调用

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

编译器选择String版本超过Object,因为print采用String的方法比采用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

当你说有两个有效的方法定义符合重载时,情况开始变得有趣。 E.g。

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

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

现在如果你调用

print(sillyGenericWrapper.get());

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

答案 3 :(得分:1)

我使用Java 1.8.0_40运行它并获得了#34; Object&#34;。

如果您要运行以下代码:

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          GenericReturnType:T

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