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

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

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

这里又来了多个黑人问号

  • -128~127怎么来的?
  • 为什么是-128 ~ 127?怎么不是-200 ~ 200呢?
  • 为什么需要缓存数据?

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

对象的初始化

通过使用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的问题,后面文章会讲到。

对象之间的比较

==

  • 基本数据类型:byte,short,char,int,long,double,float,blooean,它们之间的比较,比较是它们的值;
  • 引用数据类型:使用==比较的时候,比较的则是它们在内存中的地址(heap上的地址)。

equals

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

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

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

答案分析

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

  • 第一个比较integer1 == integer2前后都是3,通过上述的对象初始化我们可以知道integer1integer2均在缓存数组中,所以Integer直接从缓存数组中去取出地址返回,那边显而易见的,当使用==的时候则为true;
  • 第二个比较integer3 == integer4前后都是129,不在-128~127范围内,则Integer需要重新实例化,所以integer3integer4都是重新实例化的对象,对应的地址自然而然的也就不一样了,所以结果为false;
  • 第三和和第四个同理;
  • 第五个比较 Integer.parseInt("128")==Integer.valueOf("128")有意思,我们先看右边,128不在缓存内需要重新实例化一个,再看左边Integer.parseInt("128")返回值是int型,最终的比较变成了128==Integer(128),再看ByteCode,如下图,我们会发现Integer(128)最终使用的是Integer.intValue ()方法,哈哈,竟做了拆箱处理,finally,比较就变成了128==128,结果当然就是true了。
4.png

扩展

回到第二节的两个问题:

  • -128~127怎么来的?
  • 为什么是-128 ~ 127?怎么不是-200 ~ 200呢?
  • 为什么需要缓存数据?

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

实际上,在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

我的理解就是:

  • -128~127范围内的数是比较频繁使用的,JDK就增加了这一默认的范围但并不是不可变的,毕竟JDK提供了两种方法供用户自定义范围;
  • 事先做缓存还是为了提高空间和时间性能!
    (大家参照上面官方与非官方的解释,自己意会哈~)

附加题

下面的输出是什么:

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/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 英文原文:Java Integer Cache翻译地址:Java中整型的缓存机制原文作者:Java Papers翻...
    呀呀2016阅读 1,768评论 0 0
  • 最近在项目中遇到一个问题,两个值相同的Integer型值进行==比较时,发现Integer其中的一些奥秘,顺便也复...
    编码前线阅读 617评论 0 0
  • pyspark.sql模块 模块上下文 Spark SQL和DataFrames的重要类: pyspark.sql...
    mpro阅读 9,391评论 0 13
  • 本文将介绍Java中Integer的缓存相关知识。这是在Java 5中引入的一个有助于节省内存、提高性能的功能。首...
    编程鸭阅读 236评论 0 0
  • 12.26晚间记录 发现最近日更写生活琐事颇多。 不知道是不是因为怀着感恩之心看待生活中的人和事,所以才会...
    木木_52ae阅读 261评论 2 1