关于autoboxing:Java中奇怪的整数拳击

Weird Integer boxing in Java

我刚刚看到类似的代码:

1
2
3
4
5
6
7
8
9
10
11
public class Scratch
{
    public static void main(String[] args)
    {
        Integer a = 1000, b = 1000;
        System.out.println(a == b);

        Integer c = 100, d = 100;
        System.out.println(c == d);
    }
}

运行时,此代码块将打印出:

1
2
false
true

我理解为什么第一个对象是false:因为这两个对象是独立的对象,所以==比较了引用。但我不明白,为什么第二个声明会返回true?当一个整数的值在某个范围内时,是否有一些奇怪的自动氧化规则?这是怎么回事?


true行实际上是由语言规范来保证的。根据第5.1.7节:

If the value p being boxed is true,
false, a byte, a char in the range
\u0000 to \u007f, or an int or short
number between -128 and 127, then let
r1 and r2 be the results of any two
boxing conversions of p. It is always
the case that r1 == r2.

讨论还在继续,建议尽管第二行的输出是有保证的,但第一行没有(见下面最后一段引用):

Ideally, boxing a given primitive
value p, would always yield an
identical reference. In practice, this
may not be feasible using existing
implementation techniques. The rules
above are a pragmatic compromise. The
final clause above requires that
certain common values always be boxed
into indistinguishable objects. The
implementation may cache these, lazily
or eagerly.

For other values, this formulation
disallows any assumptions about the
identity of the boxed values on the
programmer's part. This would allow
(but not require) sharing of some or
all of these references.

This ensures that in most common
cases, the behavior will be the
desired one, without imposing an undue
performance penalty, especially on
small devices. Less memory-limited
implementations might, for example,
cache all characters and shorts, as
well as integers and longs in the
range of -32K - +32K.


1
2
3
4
5
6
7
8
9
10
11
public class Scratch
{
   public static void main(String[] args)
    {
        Integer a = 1000, b = 1000;  //1
        System.out.println(a == b);

        Integer c = 100, d = 100;  //2
        System.out.println(c == d);
   }
}

输出:

1
2
false
true

是的,第一个输出是为了比较引用而产生的;‘A’和‘B’——这是两个不同的引用。在点1中,实际上创建了两个引用,类似于-

1
2
Integer a = new Integer(1000);
Integer b = new Integer(1000);

第二个输出是因为当Integer在范围(从-128到127)内时,JVM试图保存内存而产生的。在第2点,没有为"d"创建新的整数类型引用。它没有为整型引用变量"d"创建新的对象,而是只为以前创建的对象分配由"c"引用的对象。所有这些都是由JVM完成的。

这些内存保存规则不仅适用于整数。为了节省内存,以下包装器对象的两个实例(通过装箱创建时)将始终为==其中它们的基元值相同-

  • 布尔
  • 字节
  • 从u0000到\u007f的字符(7f为127,十进制)
  • 短整数从-128到127


某些范围内的整数对象(我认为可能是-128到127)被缓存并重新使用。超出该范围的整数每次都会得到一个新对象。


这是一个有趣的观点。在这本书中,有效的Java建议总是为自己的类重写相等。同样,为了检查Java类的两个对象实例的相等性,总是使用等值方法。

1
2
3
4
5
6
7
8
9
10
11
public class Scratch
{
    public static void main(String[] args)
    {
        Integer a = 1000, b = 1000;
        System.out.println(a.equals(b));

        Integer c = 100, d = 100;
        System.out.println(c.equals(d));
    }
}

返回:

1
2
true
true


是的,有一个奇怪的自动氧化规则,当值在某个范围内时就开始起作用。将常量赋给对象变量时,语言定义中的任何内容都不表示必须创建新对象。它可以重用缓存中的现有对象。

实际上,为了实现这个目的,JVM通常会存储一个小整数缓存,以及boolean.true和boolean.false等值。


在Java中,拳击在整数128到127之间的范围内工作。使用此范围内的数字时,可以将其与==运算符进行比较。对于超出范围的整数对象,必须使用equals。


我的猜测是Java保留了一个已经被装箱的小整数的缓存,因为它们非常普通,它节省了大量的时间来重用现有的对象,而不是创建一个新的对象。


将int文本直接赋值给整数引用是自动装箱的一个例子,在这个例子中,编译器处理对象转换代码的文本值。

因此,在编译阶段,编译器将Integer a = 1000, b = 1000;转换为Integer a = Integer.valueOf(1000), b = Integer.valueOf(1000);

因此,实际上是Integer.valueOf()方法给了我们整数对象,如果我们看看Integer.valueOf()方法的源代码,我们可以清楚地看到该方法缓存的整数对象范围是-128到127(包括128)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 *
 * This method will always cache values in the range -128 to 127,
 * inclusive, and may cache other values outside of this range.
 *
 * @param  i an {@code int} value.
 * @return an {@code Integer} instance representing {@code i}.
 * @since  1.5
 */

 public static Integer valueOf(int i) {
     if (i >= IntegerCache.low && i <= IntegerCache.high)
         return IntegerCache.cache[i + (-IntegerCache.low)];
     return new Integer(i);
 }

因此,如果传递的int文本大于-128且小于127,那么方法不是创建和返回新的整数对象,而是从内部IntegerCache返回整数对象。

Java缓存这些整数对象,因为这个整数的范围在日常编程中被大量使用,间接地节省了一些内存。

当类由于静态块而被加载到内存中时,第一次使用时会初始化缓存。缓存的最大范围可以由-XX:AutoBoxCacheMaxjvm选项控制。

这种缓存行为不仅适用于整数对象,类似于integer.integercache,我们还分别为Byte, Short, Long, Character提供ByteCache, ShortCache, LongCache, CharacterCache

你可以在我的文章中读更多的Java整数缓存——为什么整型值(127)=整数。


如果我们检查Integer对象的源代码,就会发现valueOf方法的源代码如下:

1
2
3
4
5
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

这可以解释为什么Integer物体在-128(Integer.low至127(Integer.high范围内)在自动氧化过程中是相同的参照物。我们可以看到有一个类IntegerCache负责Integer缓存数组,它是Integer类的私有静态内部类。

还有一个有趣的例子可以帮助我们理解这种奇怪的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {

      Class cache = Integer.class.getDeclaredClasses()[0];
      Field myCache = cache.getDeclaredField("cache");
      myCache.setAccessible(true);

      Integer[] newCache = (Integer[]) myCache.get(cache);
      newCache[132] = newCache[133];

      Integer a = 2;
      Integer b = a + a;
      System.out.printf("%d + %d = %d", a, a, b); //The output is: 2 + 2 = 5    

}

在Java 5中,引入了一个新的特性来保存内存并提高整数类型对象处理的性能。整数对象在内部缓存,并通过相同的引用对象重用。

  • 这适用于-127到+127之间的整数值(最大整数值)。

  • 这个整数缓存只在自动氧化上工作。整数对象将使用构造函数生成时不缓存。

  • 欲了解更多详情,请浏览以下链接:

    整数缓存详细信息