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

Why does the Java 8 generic type inference pick this overload?

考虑以下程序:

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 被命名为 "silly" 是有原因的。我试图理解为什么编译器会这样运行,请不要告诉我愚蠢的包装器首先是一个糟糕的设计。

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

我 运行 它使用 Java 1.8.0_40 得到 "Object".

如果您运行以下代码:

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;
       }
   }
}

你会看到你得到:

Object public T com.xxx.GenericTypeInference$SillyGenericWrapper.get() ReturnType: class java.lang.Object GenericReturnType: T

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

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

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

The most specific method is chosen at compile time; its descriptor determines what method is actually executed at run time.

所以当调用

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

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

此外,如果您想调用以 Object 作为参数的方法,则可以将 return 值分配给 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 个有效的方法定义可供选择,因此你会得到编译错误,因为它不能优先选择一个方法。

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

的 JLS 中缺少该章节

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

This part of the JLS is probably the most complicated one

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


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

Consider the following method:

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

Suppose you want to invoke the method processStringList with an empty list. In Java SE 7, the following statement does not compile:

processStringList(Collections.emptyList());

The Java SE 7 compiler generates an error message similar to the following:

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

The compiler requires a value for the type argument T so it starts with the value Object. Consequently, the invocation of Collections.emptyList returns a value of type List, which is incompatible with the method processStringList. Thus, in Java SE 7, you must specify the value of the value of the type argument as follows:

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

This is no longer necessary in Java SE 8. The notion of what is a target type has been expanded to include method arguments, such as the argument to the method processStringList. In this case, processStringList requires an argument of type List

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

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


您可以查看我对与重载方法解析相关的类似问题的回答

根据JLS 15.12.2.5 Choosing the Most Specific Method

If more than one member method is both accessible and applicable to a method invocation, it is necessary to choose one to provide the descriptor for the run-time method dispatch. The Java programming language uses the rule that the most specific method is chosen.

然后:

One applicable method m1 is more specific than another applicable method m2, for an invocation with argument expressions e1, ..., ek, if any of the following are true:

  1. m2 is generic, and m1 is inferred to be more specific than m2 for argument expressions e1, ..., ek by §18.5.4.

  2. m2 is not generic, and m1 and m2 are applicable by strict or loose invocation, and where m1 has formal parameter types S1, ..., Sn and m2 has formal parameter types T1, ..., Tn, the type Si is more specific than Ti for argument ei for all i (1 ≤ i ≤ n, n = k).

  3. m2 is not generic, and m1 and m2 are applicable by variable arity invocation, and where the first k variable arity parameter types of m1 are S1, ..., Sk and the first k variable arity parameter types of m2 are T1, ..., Tk, the type Si is more specific than Ti for argument ei for all i (1 ≤ i ≤ k). Additionally, if m2 has k+1 parameters, then the k+1'th variable arity parameter type of m1 is a subtype of the k+1'th variable arity parameter type of m2.

The above conditions are the only circumstances under which one method may be more specific than another.

A type S is more specific than a type T for any expression if S <: T (§4.10).

三个选项中的第二个符合我们的情况。由于 StringObject (String <: Object) 的子类型,因此更具体。因此,该方法本身更具体。按照 JLS,此方法也是更具体最具体,由编译器选择。

在java7中,表达式是自底向上解释的(极少数例外);子表达式的含义有点像 "context free"。对于方法调用,首先解析参数的类型;然后,编译器使用该信息来解析调用的含义,例如,在适用的重载方法中选择一个获胜者。

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

显然,如果方法被重载,就会出现两难选择。因此在某些情况下,有必要先解决方法重载问题,然后再编译参数。

这是一个重大转变;一些像你这样的旧代码将成为不兼容的受害者。

解决方法是为参数提供 "target typing" "casting context"

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

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


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