Java编译器选择错误的重载

mol*_*a11 14 java

@Test
public void test() {
    MyProperties props = new MyProperties();
    props.setProperty("value", new Date());

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

public class MyProperties {
    private Map<String, Object> properties = new HashMap<String, Object>();

    public void setProperty(String name, Object value) {
        properties.put(name, value);
    }

    @SuppressWarnings("unchecked")
    public <T> 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 < strLength; i++) {
            char charAt = string.charAt(i);
            if (charAt > ' ') {
                return true;
            }
        }
        return false;
    }

}
Run Code Online (Sandbox Code Playgroud)

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

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

单元测试通过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的不同环境会以不同方式编译此代码?

Mak*_*oto 7

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

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

@SuppressWarnings("unchecked")
public <T> T getProperty(String name) {
    return (T) properties.get(name);
}
Run Code Online (Sandbox Code Playgroud)

你能一眼看出T 应该是什么吗?如果我在具有IntelliJ的Java 7兼容模式下运行那些演员,那么我会回到这个非常有帮助的地方ClassCastException:

Cannot cast java.util.Date to T

因此,这意味着,在一定程度上,爪哇知道有什么在这里下车,但它选择从而是改变投(T)(Object).

@SuppressWarnings("unchecked")
public <T> Object getProperty(String name) {
    return (Object) properties.get(name);
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,演员阵容是多余的,你可以Object从地图上找回,就像你期望的那样.然后,调用正确的重载.

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


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

泛型的使用被破坏和不正确.

你甚至不需要泛型.您的代码可以处理a String或an Object,并且您的地图Object无论如何都只包含s.

你应该只ObjectgetProperty方法返回,因为那是你无论如何只能从地图返回的东西.

public Object getProperty(String name) {
    return properties.get(name);
}
Run Code Online (Sandbox Code Playgroud)

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


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

@SuppressWarnings("unchecked")
public <T> T getProperty(String name, Class<T> clazz) {
    return (T) properties.get(name);
}
Run Code Online (Sandbox Code Playgroud)

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

StringUtils.isNullOrEmpty(props.getProperty("value", Date.class));
Run Code Online (Sandbox Code Playgroud)

现在我们绝对肯定是什么T,Java 8满足于这段代码.这仍然有点气味,因为你把东西存放在Map<String, Object>; 如果你有被Object覆盖的方法,你可以保证该地图中的所有对象都有意义toString,那么我个人会避免上面的代码.