繁体   English   中英

为什么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被命名为“傻”是有原因的。 我试图理解为什么编译器会以这种方式运行,但不要告诉我愚蠢的包装器最初是一个不好的设计。

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

在Java 8中,类型推断规则已经得到了重大改进。 最值得注意的是,目标类型推断已大大改进。 因此,尽管在Java 8之前,方法自变量站点没有收到任何推断,默认为Object,但在Java 8中,推断了最特定的适用类型,在这种情况下为String。 JLS for Java 8引入了新的章节第18章。JLS for Java 7中缺少类型推断

在早期版本的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中,您必须指定type参数值的值,如下所示:

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

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

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

这种不兼容性在《 JDK 8兼容性指南》中值得一提。


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

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

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

然后:

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

  1. m2是通用的,并且对于第18.5.4节,对于参数表达式e1,...,ek推断m1比m2更具体。

  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个可变arity参数类型为S1,...,Sk,而m2的前k个可变arity参数类型为T1,...。对于所有i(1≤i≤k),自变量ei的类型Si比Ti更具体。 另外,如果m2具有k + 1个参数,则m1个第k + 1个可变稀疏参数类型是m2个第k + 1个可变稀疏参数类型的子类型。

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

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

三个选项中的第二个匹配我们的情况。 因为StringObject的子类型( String <: Object ),所以它更加具体。 因此,该方法本身更加具体 在JLS之后,此方法也严格更具体,并且最具体 ,由编译器选择。

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

在java8中,该原理不再起作用,因为我们希望在所有地方都使用隐式lambda(例如x->foo(x) )。 未指定lambda参数类型,必须从上下文中推断出来。 这意味着,对于方法调用,有时方法参数类型决定参数类型。

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

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

一种解决方法是为带有“广播上下文”的参数提供“目标类型”

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

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


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

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

Jls, 第15节提供了许多有关编译器如何正确选择重载方法的信息。

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

所以当调用

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

编译器选择String版本过Object ,因为print是需要方法String是更具体的,则一个需要Object 如果有Integer而不是String那么它将被选中。

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

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

您会看到得到:

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM