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

2022-09-01 02:10:42

请考虑以下程序:

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 一致。这是预料之中的,但我仍然觉得有必要进行验证。


答案 1

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

早期版本的JDK 1.8(直到1.8.0_25)在编译器成功编译代码时有一个与重载方法解析相关的错误,根据JLS应该产生歧义错误为什么这种方法重载不明确?正如Marco13在评论中指出的那样

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

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


如 Java Tutoral(类型推断)中的示例所示)

请考虑以下方法:

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 中,必须按如下方式指定类型参数的值的值:

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

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

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

这种不兼容性值得在 JDK 8 的兼容性指南中提及。


您可以查看我对与重载方法解析相关的类似问题的答案 Java 8 三元条件和未装箱基元的方法重载歧义

根据 JLS 15.12.2.5 选择最具体的方法

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

然后:

一个适用的方法 m1 比另一个适用的方法 m2 更具体,用于使用参数表达式 e1, ..., ek 的调用(如果满足以下任一条件):

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

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

  3. m2 不是泛型的,m1 和 m2 通过变量 arity 调用适用,其中 m1 的第一个 k 变量 arity 参数类型是 S1, ...,Sk 和 m2 的第一个 k 个变量 arity 参数类型是 T1, ..., Tk,对于所有 i 的参数 ei,Si 类型比 Ti 更具体(1 ≤ i ≤ k)。此外,如果 m2 具有 k+1 个参数,则 m1 的第 k+1 个变量 arity 参数类型是 m2 的第 k+1'个变量 arity 参数类型的子类型。

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

对于任何表达式,如果 S <:T (§4.10),则类型 S 比类型 T 更具体。

三个选项中的第二个与我们的情况相匹配。由于 是 () 的子类型,因此它更具体。因此,方法本身更具体。遵循JLS,此方法也严格地更具体,最具体,并由编译器选择。StringObjectString <: Object


答案 2

在java7中,表达式是自下而上解释的(除了极少数例外);子表达式的含义是“上下文自由”。对于方法调用,参数的类型是解析的第一个;然后,编译器使用该信息来解析调用的含义,例如,在适用的重载方法中选择一个赢家。

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

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

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

解决方法是使用“转换上下文”为参数提供“目标类型”

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

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


Java方法重载非常复杂;复杂性的好处是值得怀疑的。请记住,重载从来都不是必需的 - 如果它们是不同的方法,则可以为它们指定不同的名称。


推荐