为什么 Long.valueOf(0).equals(Integer.valueOf(0)) 的结果是 false?

18

这个问题是由奇怪的HashMap.put()行为引起的。

我认为我理解了为什么Map<K,V>.put需要一个K,而Map<K,V>.get需要一个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 不就行了吗?

7个回答

27

这里是来自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,它有很多类似的示例。


1
也许我没有表达清楚。我知道为什么会发生这种情况,因为在源代码中有使其以这种方式发生的原因,我在发布之前已经阅读了代码。我的问题是为什么决定应该这样做? - Miserable Variable
3
因为将 Long 类型与 Integer 类型进行相等性比较可能违反对称性,所以 https://dev59.com/lnRB5IYBdhLWcg3w-8Ho 中解释得更加清楚。 - Spencer Kormos

7

一般来说,尽管在equals()的合同中没有严格表述,但对象不应将自己视为与不完全相同的类(即使它是子类)的另一个对象相等。考虑对称性-如果a.equals(b)为真,则b.equals(a)也必须为真。

让我们有两个对象,类为Superfoo和扩展SuperSub类的bar。现在考虑equals()在Super中的实现,特别是当它被调用为foo.equals(bar)时。Foo只知道bar被强制类型为Object,因此为了进行准确的比较,它需要检查它是否是Super的实例,如果不是则返回false。它是这样做的,所以这部分没问题。现在它比较所有实例字段等(或者实际比较实现的任何内容),并发现它们相等。到目前为止,还好。

然而,根据合同,只有在它知道bar.equals(foo)也会返回true时,它才能返回true。由于bar可以是Super的任何子类,不清楚equals()方法是否会被覆盖(很可能会)。因此,为确保实现正确,您需要对其进行对称编写,并确保两个对象是相同的类。
更基本地说,不同类的对象实际上不能被视为相等-例如,在这种情况下,只有一个对象可以插入到HashSet<Sub>中。

长整型和整型是不同的类类型,因此使用equals比较它们将始终返回false。 - Steve Kuo
2
对于Long l = 0和Integer i = 0,如果l.equals(i)可能为真,则i.equals(l)也可能为真。对称性得以保持。 - Miserable Variable
12
@dtsazza、Steve Kuo:我强烈不同意。事实上,在Java库中,有些地方会将不同类的对象视为相等。例如,List接口规定,如果它们的内容相等,不考虑它们的类别,不同类的列表应该是相等的;因此,一个ArrayList可以和LinkedList相互 .equals()。 - newacct
错误。equals() 的规范遵循Liskov替换原则,该原则指出子类必须能够代替其超类。核心API有很多例子都与您的答案相矛盾,比如ArrayList.equals(List)甚至是ArrayList.equals(LinkedList) - Gili

5
是的,但这完全取决于比较算法以及转换的程度。例如,当您尝试m.Contains(“5”)时,希望发生什么?或者如果您将包含5作为第一个元素的数组传递给它会怎样?简单地说,它似乎是“如果类型不同,则键也不同”的连接。
然后使用一个具有object作为键的集合。如果您put一个5L,然后尝试获取5“5”,...,您希望发生什么?如果您put一个5L、一个5和一个“5”,并且您想检查是否存在5F,那该怎么办?
由于它是通用集合(或模板化集合,或任何您希望称之为的东西),因此必须对某些值类型进行特殊比较。如果K是int,则检查传递的对象是否为longshortfloatdouble等,然后进行转换和比较。如果K是float,则检查传递的对象是否为...
你懂的。
另一种实现可能是如果类型不匹配,则抛出异常,但我经常希望它这样做。

1
我不明白重点在哪里。当然,Long等于Inter和String等于Integer之间存在区别,例如有隐式转换。 异常可能有助于捕获这种情况,是的。 - Miserable Variable

4

你的问题看起来很合理,但如果对于两种不同类型返回true,则违反了equals()的一般约定,即使不违反其契约。


0
Java 语言设计的一部分是对象不会自动转换为其他类型,这与 C++ 不同。这是使 Java 成为一种小而简单的语言的一部分。C++ 的复杂性很大程度上来自于隐式转换及其与其他特性的交互。
此外,Java 在原始类型和对象之间有一个明显的区别。这与其他语言不同,其他语言将此差异隐藏在优化下面。这意味着您不能指望 Long 和 Integer 行为类似于 long 和 int。
库代码可以编写以隐藏这些差异,但这实际上可能会损害编程环境的一致性。

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.

等等。没有必要正确获取类型,如果您做了一些愚蠢的事情(例如尝试将长整型存储在整型中),编译器会给出错误(您可以进行更正或覆盖)。

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

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

作为额外的福利,内存开销和运行时性能都更好。

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

所以你的问题的一部分是自动装箱。另一部分是你有不同类型,正如其他帖子中所述。


我确实理解自动装箱。我怀疑你没有理解我的观点,即在调用containsKey时很容易忘记L。 - Miserable Variable

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接