17

这个问题是由奇怪的 HashMap.put() 行为提示的

我想我理解为什么Map<K,V>.put需要 aKMap<K,V>.get需要 a Object,似乎不这样做会破坏太多现有代码。

现在我们进入一个非常容易出错的场景:

java.util.HashMap<Long, String> m = new java.util.HashMap<Long, String>();
m.put(5L,"Five"); // compiler barfs on m.put(5, "Five")
m.contains(5); // no complains from compiler, but returns false

Long如果值在int范围内并且值相等,则不能通过返回 true 来解决这个问题吗?

4

7 回答 7

25

这是 Long.java 的源代码

public boolean equals(Object obj) {
    if (obj instanceof Long) {
        return value == ((Long)obj).longValue();
    }
    return false;
}

即它必须是 Long 类型才能相等。我认为主要区别在于:

long l = 42L
int i = 42;
l == i

上面的示例是,对于原语,可能会隐式扩大 int 值,但是对于对象类型,没有从 Integer 隐式转换为 Long 的规则。

另请查看Java Puzzlers,它有很多类似的示例。

于 2009-01-15T08:28:23.710 回答
7

一般来说,虽然equals()的约定中没有严格表示,但对象不应认为自己与不属于完全相同类的另一个对象相等(即使它是子类)。考虑对称属性 - 如果 a.equals(b) 为真,则 b.equals(a) 也必须为真。

让我们有两个对象,fooclassSuperbarclass Sub,它们 extends Super。现在考虑equals()Super 中的实现,特别是当它被称为foo.equals(bar). Foo 只知道 bar 被强类型化为 an Object,因此为了获得准确的比较,它需要检查它是否是 Super 的实例,如果不是则返回 false。是的,所以这部分很好。它现在比较所有实例字段等(或任何实际的比较实现),并发现它们相等。到现在为止还挺好。

但是,根据合同,只有当它知道 bar.equals(foo) 也将返回 true 时,它​​才能返回 true。由于 bar 可以是 Super 的任何子类,因此尚不清楚 equals() 方法是否会被覆盖(如果可能会被覆盖)。因此,为了确保您的实现是正确的,您需要对称地编写它并确保两个对象是同一个类。

更根本的是,不同类的对象实际上不能被认为是相等的——因为在这种情况下,例如,只能将它们中的一个插入到 aHashSet<Sub>中。

于 2009-01-15T14:24:13.960 回答
5

是的,但这一切都取决于比较算法以及转换的距离。例如,当您尝试时,您希望发生什么m.Contains("5")?或者如果你传递一个以 5 作为第一个元素的数组?简单地说,它似乎是“如果类型不同,则键不同”。

然后取一个以 anobject为 key 的集合。如果你puta 5L,然后尝试得到5, "5", ...,你想发生什么?如果你puta 5Land a 5and a "5"and 你想检查 a5F怎么办?

由于它是一个通用集合(或模板化,或任何您希望调用的集合),它必须检查并针对某些值类型进行一些特殊的比较。如果 Kint则检查传递的对象是否为long, short, float, double, ..., 然后转换并比较。如果 K 是float然后检查传递的对象是否是...

你明白了。

但是,如果类型不匹配,另一种实现可能是抛出异常,我经常希望它这样做。

于 2009-01-15T08:24:08.727 回答
4

您的问题从表面上看似乎是合理的,但是如果不是它的合同,那么对于两种不同的类型返回 true 将违反 equals() 的一般约定。

于 2009-01-15T14:08:14.460 回答
0

Java 语言设计的一部分是对象永远不会隐式转换为其他类型,这与 C++ 不同。这是使 Java 成为一种小而简单的语言的一部分。C++ 复杂性的合理部分来自隐式转换及其与其他功能的交互。

此外,Java 在基元和对象之间有明显的二分法。这与其他语言不同,其他语言作为优化隐藏了这种差异。这意味着您不能期望 Long 和 Integer 的行为类似于 long 和 int。

可以编写库代码来隐藏这些差异,但这实际上会通过降低编程环境的一致性来造成伤害。

于 2009-01-15T14:22:20.203 回答
0

所以你的代码应该是....

java.util.HashMap<Long, String> m = new java.util.HashMap<Long, String>();
m.put(5L, "Five"); // compiler barfs on m.put(5, "Five")
System.out.println(m.containsKey(5L)); // true

您忘记了 java 正在自动装箱您的代码,因此上面的代码将等同于

java.util.HashMap<Long, String> m = new java.util.HashMap<Long, String>();
m.put(new Long(5L), "Five"); // compiler barfs on m.put(5, "Five")
System.out.println(m.containsKey(new Long(5))); // true
System.out.println(m.containsKey(new Long(5L))); // true

所以你的问题的一部分是自动装箱。另一部分是您有其他海报所说的不同类型。

于 2009-01-16T04:53:23.113 回答
0

其他答案充分解释了它失败的原因,但它们都没有解决如何编写围绕这个问题不太容易出错的代码。必须记住添加类型转换(没有编译器帮助),带 L 的后缀原语等等是不可接受的恕我直言。

当你有原语(以及在许多其他情况下)时,我强烈建议使用 GNU trove 集合库。例如,有一个 TLongLongHashMap 将事物内部存储为原始 long。结果,您永远不会以装箱/拆箱而告终,也永远不会以意外行为告终:

TLongLongHashMap map = new TLongLongHashMap();
map.put(1L, 45L);
map.containsKey(1); // returns true, 1 gets promoted to long from int by compiler
int x = map.get(1); // Helpful compiler error. x is not a long
int x = (int)map.get(1); // OK. cast reassures compiler that you know
long x = map.get(1); // Better.

等等。无需正确输入类型,如果您做了一些愚蠢的事情(尝试将 long 存储在 int 中),编译器会给您一个错误(您可以更正或覆盖)。

自动转换的规则意味着比较也能正常工作:

if(map.get(1) == 45) // 1 promoted to long, 45 promoted to long...all is well

作为奖励,内存开销和运行时性能要好得多。

于 2012-02-29T17:55:26.530 回答