Java编译器选择错误的重载

@Test public void test() { MyProperties props = new MyProperties(); props.setProperty("value", new Date()); StringUtils.isNullOrEmpty(props.getProperty("value")); } public class MyProperties { private Map properties = new HashMap(); public void setProperty(String name, Object value) { properties.put(name, value); } @SuppressWarnings("unchecked") public  T getProperty(String name) { return (T) properties.get(name); } } public class StringUtils { public static boolean isNullOrEmpty(Object string) { return isNullOrEmpty(valueOf(string)); } public static String valueOf(Object string) { if (string == null) { return ""; } return string.toString(); } public static boolean isNullOrEmpty(String string) { if (string == null || string.length() == 0) { return false; } int strLength = string.length(); for (int i = 0; i  ' ') { return true; } } return false; } } 

多年来,这个unit testing已经过去了。 然后在升级到Java 8之后,在某些环境中,当通过javac编译代码时,它会选择StringUtils.isNullOrEmpty(String)重载。 这会导致unit testing失败,并显示以下错误消息:

java.lang.ClassCastException: java.util.Date cannot be cast to java.lang.String at com.foo.bar.StringUtils_UT.test(StringUtils_UT.java:35)

unit testing通过ant(ant 1.9.6,jdk_8_u60,Windows 7 64bit)在我的机器上编译和运行时通过,但在另一个版本的ant和java(ant 1.9.6 jdk_8_u60,Ubuntu 12.04.4 32bit)上失败。

Java的类型推断在编译时从所有适用的重载中选择了最具体的重载,在Java 8中已经改变了。我认为我的问题与此有关。

我知道编译器将MyProperties.getProperty(…)方法的返回类型视为T,而不是Date。 由于编译器不知道getProperty(…)方法的返回类型,为什么它选择StringUtils.isNullorEmpty(String)而不是StringUtils.isNullorEmpty(Object) – 这应该总是有效?

这是Java中的错误还是Java 8类型推断更改的结果? 另外,为什么使用相同版本的java的不同环境会以不同方式编译此代码?

这段代码闻起来 。 是的,这在Java 7下传递,是的,它在Java 7上运行正常,但这里肯定存在一些错误

首先,我们来谈谈这种通用类型。

 @SuppressWarnings("unchecked") public  T getProperty(String name) { return (T) properties.get(name); } 

你能一眼看出T 应该是什么吗? 如果我在那个确切的行上使用IntelliJ以Java 7兼容模式运行那些强制转换,我会回到这个非常有用的ClassCastException

Cannot cast java.util.Date to T

所以这意味着在某种程度上,Java知道这里有一些东西,但它选择改变该转换而不是从(T)改为(Object)

 @SuppressWarnings("unchecked") public  Object getProperty(String name) { return (Object) properties.get(name); } 

在这种情况下,强制转换是多余的,您可以从地图中获取Object ,如您所料。 然后,调用正确的重载。

现在,在Java 8中,事情变得更加明智; 因为你没有真正为getProperty方法提供一个类型,所以它会爆炸,因为它真的无法将java.util.DateT


最终,我正在掩饰主要观点:

generics的使用被破坏和不正确。

你甚至不需要generics。 您的代码可以处理StringObject ,并且您的地图无论如何都只包含Object

您应该只从getProperty方法返回Object ,因为无论如何您只能从地图返回。

 public Object getProperty(String name) { return properties.get(name); } 

它确实意味着你不再能够使用String的签名直接调用该方法(因为你现在正在传递一个Object ),但这确实意味着你的破坏的generics代码最终可以被搁置。


如果你真的想要保留这种行为,你必须在你的函数中引入一个新参数,它实际上允许你指定你想从地图中返回哪种类型的对象。

 @SuppressWarnings("unchecked") public  T getProperty(String name, Class clazz) { return (T) properties.get(name); } 

然后你可以调用你的方法:

 StringUtils.isNullOrEmpty(props.getProperty("value", Date.class)); 

现在我们绝对确定T是什么,Java 8满足于此代码。 这仍然有点气味,因为你将东西存储在Map ; 如果你有Object overridden方法,你可以保证该映射中的所有对象都有一个有意义的toString ,那么我个人会避免上面的代码。

Java 8确实改进了目标类型推断 。 这意味着编译器将使用目标类型来推断类型参数。

在您的情况下,这意味着在此声明中

 StringUtils.isNullOrEmpty(props.getProperty("value")); 

Java将使用isNullOrEmpty的参数类型来确定getProperty方法的类型参数。 但是有两个isNullOrEmpty重载,一个带一个Object ,一个带一个StringT上没有绑定,因此编译器将选择匹配的最具体方法 – 带有String的重载。 T被推断为String

您对T的强制转换是未选中的,因此编译器允许它,但它会为您提供有关将Object强制转换为T的未经检查的强制转换警告。 但是,当调用isNullOrEmpty方法时,抛出类isNullOrEmptyexception,因为原始对象实际上是一个Date ,无法转换为String

这说明了忽略未经检查的投射警告的危险。

这在Java 7中没有发生,因为改进的目标类型推断不存在。 编译器推断出Object

Java 8中改进的目标类型推断显示,您的getProperty方法错误地忽略了您使用@SuppressWarnings抑制的未经检查的强制转换警告。

要解决这个问题,请不要使用带有String的重载方法。 在带有Object的重载内移动String特定的逻辑。

 public static boolean isNullOrEmpty(Object o) { // null instanceof String is false String string = (o instanceof String) ? ((String) o) : valueOf(o); if (string == null || string.length() == 0) { return false; } int strLength = string.length(); for (int i = 0; i < strLength; i++) { char charAt = string.charAt(i); if (charAt > ' ') { return true; } } return false; } 

当然这意味着getProperty方法的generics是没有意义的。 删除它们。

 public Object getProperty(String name) { return properties.get(name); }