为什么编译器/JVM不能使自动装箱“只是工作”?

13

自动装箱有点可怕。虽然我充分理解 == .equals 之间的区别,但我仍无法避免以下错误:

    final List<Integer> foo = Arrays.asList(1, 1000);
    final List<Integer> bar = Arrays.asList(1, 1000);
    System.out.println(foo.get(0) == bar.get(0));
    System.out.println(foo.get(1) == bar.get(1));

那会打印出来

true
false
为什么他们要这样做?这与缓存的整数有关,但如果是这样,为什么不仅缓存程序使用的所有整数?或者为什么JVM不总是自动取消装箱成原始类型?
打印false false或true true会更好。
编辑
我不同意旧代码的失效。通过使foo.get(0) == bar.get(0)返回true,您已经破坏了代码。
这不能在编译器级别解决,只需在字节码中将Integer替换为int(只要它从未被分配为null)。

9
它能够工作!但并不是你预期的方式 ;) - OscarRyz
实际上,你的示例与自动装箱关系不大,该行为早在其之前就存在了。 自动装箱确实迫使我们更加意识到它:equals()是比较对象的(通常正确的)方式,== 是比较原始数据类型的(唯一)方式...自动装箱帮助程序员将 Integer 和 int (几乎)可以互换使用...因此这里存在错误的危险。 - leonbloy
这可能是由于影响String实例的特殊性质所致。据我所知,根据值在幕后进行了一些池化。这可以通过使用“new”关键字来防止。 - James P.
如果您正在使用Eclipse,可以在“首选项>Java>编译器>错误/警告”中启用编译器警告:“装箱和拆箱转换”来处理这些情况。也许其他IDE中也有类似的警告,但据我所知,在命令行编译时不可用。 - Chris Lercher
1
我认为你的代码有bug,而不是javac或JVM的问题。请参见下面@alexander-pogrebnyak的回复。我同意有些混淆和不幸的缓存。但是foo.get和bar.get返回对整数的对象引用。您的列表被定义为包含整数对象。因此,除非您非常清楚并确定为什么不这样做,并完全了解基于语言和JVM的工作原理可以期望什么结果,否则应始终执行.equals比较。这就是Java的工作方式。如果您不喜欢它,请正确编写代码! - nicerobot
6个回答

13
  • 他们为什么要这样做?

Java将-128到127之间的每个整数都缓存起来,据说是为了提高性能。即使现在他们想改变这个决定,也很难做到。如果有人构建了依赖于此的代码,当它被取出时,他们的代码将会崩溃。对于业余编程来说,这或许无关紧要,但对于企业代码来说,人们会感到不满并会发生诉讼。

  • 为什么不缓存程序中使用的所有整数?

所有的整数都无法被缓存,因为这将带来巨大的内存影响。

  • JVM为什么不总是自动拆箱为原始类型?

因为JVM无法知道您想要什么。此外,这种更改可能会破坏未能处理此情况的旧代码。

如果JVM在调用==时自动对对象进行拆箱,这个问题实际上会变得更加混乱。现在你需要记住,除非对象可以被拆箱,否则==始终比较对象引用。这将导致更多奇怪混乱的情况,就像你上面提到的那个问题一样。

与其过于担心这个问题,不如记住以下规则:

永远不要使用==比较对象,除非您打算按照它们的引用进行比较。如果您这样做,我想不出在哪种情况下会出现问题。


1
永远不要说“永远不”。你应该使用==比较枚举值。 - Alexander Pogrebnyak
2
有道理。那么你仍然拥有你的“kill -9许可证”:)。007离开。 - Alexander Pogrebnyak
1
你什么时候可能不想要对基元类型进行取消装箱操作? - Chris
这个Java整数缓存行为有相应的参考资料吗? - NobleUplift
此外,我不明白这如何回答原始问题,因为OP的测试落在您提出的范围内。 - NobleUplift
显示剩余2条评论

7

如果每个Integer都携带着internment的开销,你能想象性能会有多糟糕吗?同时,这也不适用于new Integer

Java语言(而非JVM问题)不能总是自动拆箱,因为设计用于1.5之前的Java代码仍然需要运行。


再次提到向后兼容性问题,加一分。 - Chris
因为在我的例子中 == 返回true。在自动装箱之前,它会返回false。 - Pyrolistical

5

Byte范围内的Integer是相同的对象,因为它们被缓存了。超出Byte范围的Integer不是相同的对象。如果要缓存所有整数,想象一下所需的内存。

并且来自这里

所有这些魔法的结果是,您可以在很大程度上忽略int和Integer之间的区别,但有一些注意事项。一个Integer表达式可以具有null值。如果您的程序尝试自动取消包装null,则会抛出NullPointerException。==运算符对Integer表达式执行引用标识比较,对int表达式执行值相等比较。最后,即使自动完成装箱和拆箱,也会带来性能成本。


在[-128, 127]之外的Integer可能会被内部化,也可能不会。 (我认为原始问题理解正在发生的事情,但想知道为什么?) - Tom Hawtin - tackline
我认为它们不是使用Sun的实现。无论如何,这与缓存它们以及缓存所需的资源有关。 - Bozho

4
如果完全跳过自动装箱,您仍然会得到这种行为。
final List<Integer> foo =
  Arrays.asList(Integer.valueOf( 1 ), Integer.valueOf( 1000 ));
final List<Integer> bar =
  Arrays.asList(Integer.valueOf( 1 ), Integer.valueOf( 1000 ));

System.out.println(foo.get(0) == bar.get(0)); // true
System.out.println(foo.get(1) == bar.get(1)); // false

如果您想要特定的行为,请更加明确:

final List<Integer> foo =
  Arrays.asList( new Integer( 1 ), new Integer( 1000 ));
final List<Integer> bar =
  Arrays.asList( new Integer( 1 ), new Integer( 1000 ));

System.out.println(foo.get(0) == bar.get(0)); // false
System.out.println(foo.get(1) == bar.get(1)); // false

这就是Eclipse默认开启自动装箱警告的原因之一。

我的Eclipse副本默认没有开启它,而且我也没有更改过。你使用的是哪个版本?我已经检查了3.2和3.4。 - Chris
@Crhis:Eclipse Gallileo(3.5)Window->Preferences Java->Compiler->Errors/Warnings->Potential Programming Problems->Boxing and Unboxing Conversions。也许默认情况下它没有开启,但自从我切换到Java 5以来,我一直开启它。 - Alexander Pogrebnyak
它绝对不是默认开启的。我在这里阅读了相关信息后才进行了更改。 - Jeremy Goodell
但它似乎没有做我想象中的那样。我的代码是: if (tsUser.user.id!= connectedUser()。id) 但它没有给我警告。两个id都是长整型(Longs)。 - Jeremy Goodell

3
很多人在这个问题上遇到了困难,甚至包括写Java书籍的人。
在《Pro Java Programming》中,作者在讨论使用自动装箱的整数作为IdentityHashMap键时遇到的问题时,他在WeakHashMap中使用了自动装箱的Integer键。他使用的示例值大于128,因此垃圾回收调用成功。但是,如果有人使用他的示例并使用小于128的值,则他的示例将失败(由于键被永久缓存)。

2

当你编写代码时

foo.get(0)

编译器不在乎你是怎么创建List的。它只关注foo的编译时类型。所以,如果foo的类型是List<Integer>,那么它将把它视为List<Integer>,并且List<Integer>的get()方法总是返回一个整数。如果你想使用==操作符,那么你必须这样写:

System.out.println(foo.get(0).intValue() == bar.get(0).intValue());

不是

System.out.println(foo.get(0) == bar.get(0));

因为这具有完全不同的含义。

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