程序员在成长Java 杂谈

面试系列之Integer缓存所引发的惨案(保证看完你就彻底明白)

2019-08-28  本文已影响0人  冬天只爱早晨

今天在整理代码的时候发现了一段程序,如下

Integer integer1 = 3;
Integer integer2 = 3;

if (integer1 == integer2)
  System.out.println("integer1 == integer2");
else
  System.out.println("integer1 != integer2");

Integer integer3 = 129;
Integer integer4 = 129;

if (integer3 == integer4)
  System.out.println("integer3 == integer4");
else
  System.out.println("integer3 != integer4");

System.out.println(Integer.valueOf("127")==Integer.valueOf("127"));
System.out.println(Integer.valueOf("128")==Integer.valueOf("128"));
System.out.println(Integer.parseInt("128")==Integer.valueOf("128"));

我看了一眼,只记得答案是

integer1 == integer2
integer3 != integer4
true
false
true

我刚想把代码关闭,脑海里突然黑人问号?

为什么会是这样?

还别说,我竟然一时语噻,说不出个所以然来,考虑到这是一个面试题,而且也想去了解下设计原理,索性花了点时间去重新整理了一下,做个记录!

说明:本文使用的Java版本号如下

java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

缓存

这里考察的主要是包装类型的缓存,我们点开Integer的源码,会发现如下代码

private static class IntegerCache {
  // 最小值
  static final int low = -128;
  // 最大值,支持自定义
  static final int high;
  // 缓存数组
  static final Integer cache[];

  static {
    // 最大值可以通过属性配置来改变
    int h = 127;
    String integerCacheHighPropValue =
      sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    // 如果设置了对应的属性,则使用该值
    if (integerCacheHighPropValue != null) {
      try {
        int i = parseInt(integerCacheHighPropValue);
        i = Math.max(i, 127);
        // 最大数组大小为Integer.MAX_VALUE
        h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
      } catch( NumberFormatException nfe) {
        // If the property cannot be parsed into an int, ignore it.
      }
    }
    high = h;
        
    cache = new Integer[(high - low) + 1];
    int j = low;
    // 将low-high范围内的值全部实例化并存入数组中当缓存使用
    for(int k = 0; k < cache.length; k++)
      cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
  }

  private IntegerCache() {}
}

这里就是Integer包装类型里的缓存声明:

Integer第一次使用的时候就会初始化缓存,其中范围最小值为-128,最大值默认是127,它可以通过-Djava.lang.Integer.IntegerCache.high=xxx或者-XX:AutoBoxCacheMax=xxx来设置,如下图:

1.png 2.jpg

接着会把low至high中所有的数据初始化存入数据中,默认就是将-128~127总共256个数循环实例化存入cache数组中。准确的说应该是将这256个对象在内存中的地址存进数组中。

再来看看其他包装类型中缓存的支持;

基本类型 大小 最小值 最大值 包装器类型 缓存范围 是否支持自定义
boolean - - Bloolean
char 6bit Unicode 0 Unic ode 2(16)-1 Character 0~127
byte 8bit -128 +127 Byte -128~127
short 16bit -2(15) 2(15)-1 Short -128~127
int 32bit -2(31) 2(31)-1 Integer -128~127 支持
long 64bit -2(63) 2(63)-1 Long -128~127
float 32bit IEEE754 IEEE754 Float -
double 64bit IEEE754 IEEE754 Double -
void - - - Void

这里又来了多个黑人问号

后面会讲到,我们先把程序走完~

对象的初始化

通过使用IDEA的Show ByteCode我们可以得到代码在JVM里的亚子,如图

3.png

我们可以看到Integer integer1 = 3; 实际上是通过Integer.valueOf返回一个Integer对象,我们再进入源码,它的最终现实如下:

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

很明显了,首先判断i是否在已经被缓存,如果是的话直接从缓存中取出(地址)返回,否则就重新实例化一个。
注意: 恶心的就在这里,有就从缓存拿,没有就实例化!这也就牵扯出Integer Swap的问题,后面文章会讲到。

对象之间的比较

==

equals

这个是Object下的一个方法,对应代码如下:

public boolean equals(Object obj) {
  return (this == obj);
}

默认也是比较两个对象之间的内存地址,而其他对象在继承Object的时候一般会去Overwrite此方法,所以在没有Overwrite的情况下用equals的结果跟==一样都是比较内存地址,否则则按照Overwrite规则来。

答案分析

再来看看本处的代码,使用==则认为是比较前后两者的内存地址(以下比较均使用默认的缓存大小即-128~127)

4.png

扩展

回到第二节的两个问题:

后来在网上查找,找到一个比较靠谱的解释

实际上,在Java 5中首次引入此功能时,范围固定为-127到+127。 后来在Java 6中,范围的最大值映射到java.lang.Integer.IntegerCache.high,VM参数允许我们设置高位数。 根据我们的应用用例,它可以灵活地调整性能。 应该从-127到127选择这个数字范围的原因应该是什么。这被认为是广泛使用的整数范围。 在程序中首次使用Integer必须花费额外的时间来缓存实例。

Java Language Specification 的说明如下:

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 char and short values, as well as int and long values in the range of -32K to +32K.

API 上解释

Returns an Integer instance representing the specified int value. If a new Integer instance is not required, this method should generally be used in preference to the constructor Integer(int), as this method is likely to yield significantly better space and time performance by caching frequently requested values. This method will always cache values in the range -128 to 127, inclusive, and may cache other values outside of this range

我的理解就是:

附加题

下面的输出是什么:

Integer integer5 = new Integer(3);
Integer integer6 = new Integer(3);
if (integer5 == integer6)
  System.out.println("integer5 == integer6");
else
  System.out.println("integer5 != integer6");

本文的示例代码: Github

参考:

https://stackoverflow.com/questions/20897020/why-integer-class-caching-values-in-the-range-128-to-127

https://stackoverflow.com/questions/20877086/why-do-comparisons-with-integer-valueofstring-give-different-results-for-12

https://docs.oracle.com/javase/specs/jls/se7/html/jls-5.html#jls-5.1.7

https://javapapers.com/java/java-integer-cache/

上一篇 下一篇

猜你喜欢

热点阅读