为什么这些是 == 而不是 `equals()`?

24

我对Java处理==equals()intInteger和其他数字类型方面的方式感到有些困惑。例如:

Integer X = 9000;
int x = 9000;
Short Y = 9000;
short y = 9000;
List<Boolean> results = new ArrayList<Boolean>();
// results.add(X == Y); DOES NOT COMPILE        1)
results.add(Y == 9000);                      // 2)
results.add(X == y);                         // 3)
results.add(X.equals(x));                    // 4)
results.add(X.equals(Y));                    // 5)
results.add(X.equals(y));                    // 6)
System.out.println(results);

输出结果(也许您可以先猜一下):

[true, true, true, false, false]
  1. 由于X == Y是不同的对象,所以无法编译通过,这是可以预料到的。
  2. 我有点惊讶Y == 9true,因为9默认是一个int类型,而且1)甚至都不能编译。请注意,您不能将int放入期望Short的方法中,但它们在此处相等。
  3. 由于相同的原因,这与第二个问题一样令人惊讶,但似乎更糟糕。
  4. 由于x是自动装箱为Integer,因此这并不奇怪。
  5. 由于不同类中的对象不应该是equal()的,因此这并不奇怪。
  6. 什么?X == ytrue,但X.equals(y)false?难道==不应该总是比equals()更严格吗?

如果有人能帮我理解这些,我会非常感激。为什么==equals()会表现出这种方式呢?

编辑:我已将9更改为9000,以显示此行为与从-128到127的整数的任何不寻常方式无关。

第二次编辑:好吧,如果您认为自己理解这些内容,请考虑以下内容,以确保自己理解正确:

Integer X = 9000;
Integer Z = 9000;
short y = 9000;
List<Boolean> results = new ArrayList<Boolean>();
results.add(X == Z);                      // 1)
results.add(X == y);                      // 2)
results.add(X.equals(Z));                 // 3)
results.add(X.equals(y));                 // 4)
System.out.println(results);

输出:

[false, true, true, false]

我所理解的原因:

  1. 由于是不同的实例,所以不同。
  2. X解压后,值相同,因此相等。
  3. 值相同,所以相等。
  4. y不能被装箱为Integer,因此不能相等。

原始问题是 X = x = Y = y = 9,在整数/短整数的“缓存区”中。 - dfa
五年后回到这个问题,我发现我已经不知道该期望什么了。很高兴我不再浪费时间考虑这种无聊的事情,现在使用Python。 - Eric Wilson
8个回答

22

整型(小的)实例被缓存,因此对于小实例保持不变式x == y(实际上是-127到+128,取决于JVM):

Integer a = 10;
Integer b = 10;

assert(a == b); // ok, same instance reused

a = 1024;
b = 1024;

assert(a == b); // fail, not the same instance....
assert(a.equals(b)); // but same _value_

编辑

第4和5个返回false是因为equals检查类型:X是一个整数,而Y是一个短整型。 这是java.lang.Integer#equals方法的作用:

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }

    return false;
}

1
我可以完整地回答你的问题:
  • X.equals(y); 会将 y 装箱为 Short。
- Vanger
1
@bruno:Integer.valueOf是一个不错的开始;然后参考http://java.sun.com/docs/books/jls/third_edition/html/conversions.html#5.1.7。 - dfa
谢谢。那对我帮助很大。当事情如此简单时,我正在看更低层次。 - bruno conde
由于Integer.valueOf()缓存,哪个问题中的表达式会是真的?在我看来,这似乎回答了一个从未被问过的问题。 - waxwing
对于修改后的问题,你说的是正确的;对于原始问题(X在缓存范围内),这也是正确的 :) - dfa
显示剩余2条评论

13

原因是

X == y

成为真实与二进制数值提升有关。当至少一个操作数可以转换为数字类型时,将使用数字等式运算符。首先,第一个操作数被取消包装。然后,两个操作数都转换为int

同时,

X.equals(y)

这是一个普通的函数调用。正如已经提到的,y将会自动装箱为一个Short对象。Integer.equals如果参数不是一个Integer实例,总是返回false。这可以通过检查实现来轻松地看出。

有人可能会认为这是一个设计缺陷。


1
@waxwing:“有人可能会认为这是一个设计缺陷”...由于听取了那些呼吁在Java中加入C#功能的人的意见而导致的。 - Stephen C
(不过,C#没有自动拆箱;在C#中,拆箱始终是显式的。) - Ruben
不同的情况需要不同的相等语义。 ==equals 的差异所导致的混淆主要来自误以为它们应该是等价的。 另一方面,如果我可以自由选择,== 运算符将明确禁止某些操作数的组合,即使在其中一个可以被隐式转换为另一个的情况下也是如此。例如,将 doublelongfloat 进行比较可以通过将 double 转换为另一种类型或将另一种类型转换为 double 来进行。哪个操作是正确的取决于具体情况。 - supercat
Java的实现方式是通过将两个操作数转换为“double”来执行所需的比较,有些情况下这可能是正确的行为,但在其他情况下则不然。要求程序执行显式强制转换可以避免歧义。有些比较只有一个明智的行为(例如,在shortint之间),不应该引起麻烦,但在我看来,那些可能导致多种不同行为的比较应该被彻底禁止。 - supercat
如果参数不是一个Integer实例,Integer.equals总是返回false。提到这一点加1分。 - Gerardo Cauich
显示剩余3条评论

7
故事寓意:
自动装箱/拆箱和类型提升都很令人困惑,虽然它们可以用来出题目,但却会导致可怕的代码。
实际上,使用比 int 更小的数字类型很少有意义,我几乎要配置我的 Eclipse 编译器将所有的自动装箱和拆箱标记为错误。

对于autoboxing的可怕之处表示赞同。 它与其他语言特性(如varargs、整数提升)产生非常恶劣的交互作用,并且小整数自动装箱到相同的引用这一“特性”极其令人困惑。 - Simon Nickerson
1
+1 尽管存在混淆的可能性,我仍然发现装箱/拆箱很有用。我尽量不使用装箱类型来处理单个项目,这样就可以消除大部分问题。剩下的问题是例如在一组长整型值中查找整型值而找不到。 - starblue

3

你的问题不仅在于它如何处理 ==,还在于自动装箱... 当你比较 Y 和 9 时,你比较的是两个相等的原始类型,而在最后两种情况下,你得到 false,只是因为 equals 的工作原理。只有当两个对象是相同类型且具有相同值时,它们才相等。 当你说 "X.equals(y)" 时,你告诉它执行 Integer.equals(Short),并查看 Integer.equals() 的实现,它将失败:

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

由于自动装箱,最后两者将导致相同的失败,因为它们都将作为 Shorts 传递。 编辑:忘记了一件事...在 results.add(X == y) 的情况下,它将取消装箱 X 并执行 (X.intValue() == y),这也是真实的,因为 9 == 9。

1
Java会自动将Integer转换为int,如果需要的话。Short也是一样。这个功能被称为自动装箱和自动拆箱。您可以在这里阅读相关内容here
这意味着当您运行代码时:
int a = 5;
Integer b = a;
System.out.println(a == b);

Java 将其转换为:

int a = 5;
Integer b = new Integer(a);
System.out.println(a == b.valueOf());

我认为它不会自动拆箱到==运算符中,而且无论如何,这个问题并没有指定Java版本。 - Chii
1
@Chii:它不需要指定版本;在5.0之前的任何版本中,Integer X = 9;都无法编译。 - Alan Moore
2
为了重复使用实例,应该使用Integer b = Integer.valueOf() - dfa

1
稍微详细地介绍一下自动装箱的工作原理以及如何缓存“小”值的整数对象:
当一个原始的 int 被自动装箱成一个 Integer 时,编译器会通过将代码替换为 Integer.valueOf(...) 的调用来完成。因此,以下代码:
Integer a = 10;

编译器将其替换为以下内容:

Integer a = Integer.valueOf(10);

类 Integer 的 valueOf(...) 方法维护一个缓存,包含所有介于 -127 和 128 之间的整数对象。如果您调用带有此范围内值的 valueOf(...) 方法,则该方法返回缓存中预先存在的对象。如果值超出此范围,则返回使用指定值初始化的新的 Integer 对象。(如果您想了解其确切工作原理,请查找 JDK 安装目录下的文件 src.zip,并查找其中的 java.lang.Integer 类源代码。)

现在,如果您执行以下操作:

Integer a = 10;
Integer b = 10;
System.out.println(a == b);

你会看到打印出了true,但这并不是因为a和b具有相同的值,而是因为a和b引用了同一个Integer对象,即由Integer.valueOf(...)返回的缓存中的对象。

如果你改变这些值:

Integer a = 200;
Integer b = 200;
System.out.println(a == b);

然后打印false,因为200超出了缓存范围,所以a和b指向两个不同的Integer对象。
很遗憾,在Java中用==进行对象相等性判断时,对于值类型如包装类和字符串来说,这是违反直觉的。

1

这种自动转换被称为自动装箱。


实际上,这被称为“装箱转换”。自动装箱是一个多余的说法。在JLS中没有出现过自动装箱这个术语。尽管如此,很多人仍然经常写它。 - Mishax

1

我记得一个好的覆盖“equal(object obj)”方法的实践是首先检查传入参数的类型。 所以也许这会导致X.equals(Y)false。 您可能需要检查源代码以发现真相 :)


1
另外,一般来说,如果 X.equals(Y) 成立,那么 Y.equals(X) 也应该成立,这就是为什么实现通常会检查对象是否是完全相同类型的原因。 - Steve Reed

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