考虑以下计划:
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一致。这是预期的,但我仍然觉得需要验证。
答案 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的调用 以下任何一种情况都是正确的:
m2是通用的,推断m1比m2更具体 参数表达式e1,...,ekby§18.5.4。
m2不是通用的,m1和m2适用于严格或宽松 调用,其中m1具有形式参数类型S1,...,Sn和m2 具有形式参数类型T1,...,Tn,类型Si更具体 比所有i的参数ei都要Ti(1≤i≤n,n = k)。
- 醇>
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。
上述条件是一种方法可能比另一种方法更具体的唯一情况。
如果S&lt;:T(§4.10),则类型S比任何表达式的类型T更具特异性。
三个选项中的第二个与我们的案例相符。由于String
是Object
(String <: 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。