【问题标题】:Java compiler choosing wrong overload [duplicate]Java编译器选择错误的重载[重复]
【发布时间】:2015-09-10 16:27:47
【问题描述】:
@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;
    }

}

多年来,这个单元测试一直通过。然后升级到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 32 位)。

Java 的 type inference 在编译时从所有适用的重载中选择最具体的重载,已在 Java 8 中更改。我认为我的问题与此有关。

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

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

【问题讨论】:

  • 在您的情况下,所有 Java-8 JRE 都应仅选择 StringUtils.isNullOrEmpty(String) 重载。检查 this SO 讨论在这种情况下将选择哪个重载...
  • 老实说getProperty 对我来说似乎是个糟糕的笑话。尽管它在 Java 8 中看起来像是一个问题,但您现在最好注意一下那个臭代码。由于除了Object 之外,您没有为value 使用任何其他类型,因此T 只能是Object。你能解释一下T 会发生什么魔法吗?如果不是,则将其删除并返回properties.get(name) 作为Object。顺便说一句,忽略警告很少是个好主意:)。
  • 是的,您的代码一直被破坏,但它发生可以工作。现在是回报的时候了。

标签: java


【解决方案1】:

这段代码有味道。是的,这在 Java 7 下通过了,是的,它在 Java 7 下运行正常,但是这里有一些肯定是错误的

首先,我们来谈谈这个泛型类型。

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

你能一眼就推断出T 应该是什么吗?如果我在 Java 7 合规模式下使用 IntelliJ 在该确切行运行这些转换,我会得到这个非常有用的ClassCastException

Cannot cast java.util.Date to T

所以这意味着在某种程度上,Java 知道这里有问题,但它选择将转换从 (T) 改为 (Object)

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

在这种情况下,演员表是多余的,正如您所期望的那样,您从地图中返回 Object。然后,调用正确的重载。

现在,在 Java 8 中,事情变得更加理智了;由于您没有真正为 getProperty 方法提供类型,因此它会崩溃,因为它真的无法将 java.util.Date 转换为 T


最后,我只是在掩饰重点:

这种对泛型的使用是错误的。

这里你甚至 需要 泛型。您的代码可以处理StringObject,而且您的地图无论如何都只包含Objects。

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

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

这确实意味着您不再能够直接调用带有String签名的方法(因为您现在正在传递Object),但确实如此意味着你损坏的泛型代码终于可以被搁置了。


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

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

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

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

现在我们完全确定T 是什么,Java 8 对这段代码很满意。这仍然有点气味,因为您将东西存储在Map&lt;String, Object&gt;;如果你有 Object 被覆盖的方法,并且你可以保证该映射中的所有对象都有一个有意义的 toString,那么我个人会避免使用上面的代码。

【讨论】:

  • 他在将字符串放入地图时可能会将它们转换为对象。但是,重载作用于声明的类型,而不是类的实际类型。据推测,泛型的使用是为了让它调用正确的类型,而不是仅仅在 isNullOrEmpty 中使用instanceof
  • 不,根本没有进行插入的强制转换。至少,这是测试表明的。如果这种行为确实发生了,那么它应该在测试中被捕获。
  • 这对我来说是不好的措辞,因为您不需要强制转换某些东西来将其传递给采用 Object(用于 setProperty)的方法。
  • 这根本不能回答问题。
【解决方案2】:

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

在你的情况下,这意味着在这个声明中

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

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

您对T 的强制转换是未经检查的,因此编译器允许这样做,但它会给您一个未经检查的强制转换警告,提示您将Object 强制转换为T。但是,当调用isNullOrEmpty方法时,会抛出类转换异常,因为原来的对象确实是Date,无法转换为String

这说明了忽略 unchecked cast 警告的危险。

这在 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 方法上的泛型是没有意义的。删除它们。

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

【讨论】:

  • ....如果您考虑一下,这是有道理的。毕竟,如果您使用泛型,您多久将泛型类型设置为Object?从字面上看,这样做的唯一原因是避免编译器警告不要使用非泛型类型。
  • 我对 Java 8 的某种发行说明关于编译器的这种行为变化有一个模糊的记忆,但现在似乎无法找到它。如果您可以提供指向它的链接,则可以加分!
  • @Lii 转到重复的问题以获得所有精彩的细节。不过,您可能会后悔自己的好奇心。
  • @MarkoTopolnik:啊,那个老问题!我喜欢那些奇怪的小细节!我找到了发行说明的链接,它是Compatibility Guide
猜你喜欢
  • 2014-06-02
  • 1970-01-01
  • 2016-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多