《阿里巴巴Java开发手册》有一段关于包装对象之间值的比较问题的规约 :

【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较。 说明:对于 Integer var = ? 在 - 128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产 生,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。

1. Integer 缓存问题分析

我们先看下面的示例代码,并思考该段代码的输出结果:

  1. public class IntTest {
  2. public static void main(String[] args) {
  3. Integer a = 100, b = 100, c = 150, d = 150;
  4. System.out.println(a == b);
  5. System.out.println(c == d);
  6. }
  7. }

通过运行代码可以得到答案,程序输出的结果分别为: true , false因为缓存了 -128 到 127 之间的数值

2. 源码分析

我们知道,Integer var = ? 形式声明变量,会通过 java.lang.Integer#valueOf(int) 来构造 Integer 对象。

  1. static final int low = -128;
  2. static final int high; //默认是127 但是可以通过配置修改
  3. public static Integer valueOf(int i) {
  4. if (i >= IntegerCache.low && i <= IntegerCache.high)
  5. return IntegerCache.cache[i + (-IntegerCache.low)];
  6. return new Integer(i);
  7. }

通过源码可以看出,如果用 Ineger.valueOf(int) 来创建整数对象,参数大于等于整数缓存的最小值(
IntegerCache.low )并小于等于整数缓存的最大值( IntegerCache.high), 会直接从缓存数组 (
java.lang.Integer.IntegerCache#cache) 中提取整数对象;否则会 new 一个整数对象。

2.1 缓存的区间的最大值和最小值是多少?

-128 127

2.2那么为什么会缓存这一段区间的整数对象呢?

通过注释我们可以得知:如果不要求必须新建一个整型对象,缓存最常用的值(提前构造缓存范围内的整型对象),会更省空间,速度也更快。

这给我们一个非常重要的启发:

如果想减少内存占用,提高程序运行的效率,可以将常用的对象提前缓存起来,需要时直接从缓存中提取。

2.3 Integer 缓存的区间可以修改吗?

java.lang.Integer.IntegerCache 是Integer的一个内部类

  1. /**
  2. * Cache to support the object identity semantics of autoboxing for values between
  3. * -128 and 127 (inclusive) as required by JLS.
  4. *
  5. * The cache is initialized on first usage. The size of the cache
  6. * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
  7. * During VM initialization, java.lang.Integer.IntegerCache.high property
  8. * may be set and saved in the private system properties in the
  9. * sun.misc.VM class.
  10. */
  11. private static class IntegerCache {
  12. static final int low = -128;
  13. static final int high;
  14. static final Integer cache[];
  15. static {
  16. // high value may be configured by property
  17. int h = 127;
  18. String integerCacheHighPropValue =
  19. sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
  20. // 省略其它代码
  21. }
  22. // 省略其它代码
  23. }

通过 IntegerCache 代码和注释我们可以看到,最小值是固定值 -128, 最大值并不是固定值,缓存的最大值是可以通过虚拟机参数 -XX:AutoBoxCacheMax=<size>}-Djava.lang.Integer.IntegerCache.high=<value> 来设置的,未指定则为 127。因此可以通过修改这两个参数其中之一,让缓存的最大值大于等于 150。
如果作出这种修改,示例的输出结果便会是: true,true

这段注释也解答了为什么要缓存这个范围的数据:

是为了自动装箱时可以复用这些对象。

我们可以参考 JLS(Java Language Specification Java语言规范) 的 Boxing Conversion 部分的相关描述。

在 -128 到 127 (含)之间的 int 类型的值,或者 boolean 类型的 true 或 false, 以及范围在’\u0000’和’\u007f’ (含)之间的 char 类型的数值 p, 自动包装成 a 和 b 两个对象时, 可以使用 a == b 判断 a 和 b 的值是否相等。

3. 反汇编法

首先编译源代码:javac IntTest.java
然后需要对代码进行反汇编,执行:javap -c IntTest
反编译后,我们得到以下代码:

  1. Compiled from "IntTest.java"
  2. public class com.chujianyun.common.int_test.IntTest {
  3. public com.chujianyun.common.int_test.IntTest();
  4. Code:
  5. 0: aload_0
  6. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  7. 4: return
  8. public static void main(java.lang.String[]);
  9. Code:
  10. 0: bipush 100
  11. 2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  12. 5: astore_1
  13. 6: bipush 100
  14. 8: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  15. 11: astore_2
  16. 12: sipush 150
  17. 15: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  18. 18: astore_3
  19. 19: sipush 150
  20. 22: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  21. 25: astore 4
  22. 27: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
  23. 30: aload_1
  24. 31: aload_2
  25. 32: if_acmpne 39
  26. 35: iconst_1
  27. 36: goto 40
  28. 39: iconst_0
  29. 40: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
  30. 43: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
  31. 46: aload_3
  32. 47: aload 4
  33. 49: if_acmpne 56
  34. 52: iconst_1
  35. 53: goto 57
  36. 56: iconst_0
  37. 57: invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
  38. 60: return
  39. }

可以明确得 “看到” 这四个 `Integer var = ?形式声明的变量的确是通过java.lang.Integer#valueOf(int)来构造Integer 对象的。


接下来对汇编后的代码进行详细分析,如果看不懂可略过:

根据《Java Virtual Machine Specification : Java SE 8 Edition》,后缩写为 JVMS , 第 6 章 虚拟机指令集的相关描述以及《深入理解 Java 虚拟机》 414-149 页的 附录 B “虚拟机字节码指令表”。 我们对上述指令进行解读:
偏移为 0 的指令为:bipush 100 ,其含义是将单字节整型常量 100 推入操作数栈的栈顶;

偏移为 2 的指令为:invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 表示调用一个 static 函数,即 java.lang.Integer#valueOf(int)

偏移为 5 的指令为:astore_1 ,其含义是从操作数栈中弹出对象引用,然后将其存到第 1 个局部变量 Slot 中;

偏移 6 到 25 的指令和上面类似;

偏移为 30 的指令为 aload_1 ,其含义是从第 1 个局部变量 Slot 取出对象引用(即 a),并将其压入栈;

偏移为 31 的指令为 aload_2 ,其含义是从第 2 个局部变量 Slot 取出对象引用(即 b),并将其压入栈;

偏移为 32 的指令为 if_acmpn,该指令为条件跳转指令,if_ 后以 a 开头表示对象的引用比较。
由于该指令有以下特性:

if_acmpeq 比较栈两个引用类型数值,相等则跳转 if_acmpne 比较栈两个引用类型数值,不相等则跳转

由于 Integer 的缓存问题,所以 a 和 b 引用指向同一个地址,因此此条件不成立(成立则跳转到偏移为 39 的指令处),执行偏移为 35 的指令。

偏移为 35 的指令: iconst_1,其含义为将常量 1 压栈( Java 虚拟机中 boolean 类型的运算类型为 int ,其中 true 用 1 表示,详见 2.11.1 数据类型和 Java 虚拟机

然后执行偏移为 36 的 goto 指令,跳转到偏移为 40 的指令。

偏移为 40 的指令:invokevirtual #4 // Method java/io/PrintStream.println:(Z)V
可知参数描述符为 Z ,返回值描述符为 V

根据 4.3.2 字段描述符 ,可知 FieldType 的字符为 Z 表示 boolean 类型, 值为 truefalse
根据 4.3.3 字段描述符 ,可知返回值为 void

因此可以知,最终调用了 java.io.PrintStream#println(boolean) 函数打印栈顶常量即 true

然后比较执行偏移 43 到 57 之间的指令,比较 c 和 d, 打印 false

执行偏移为 60 的指令,即 retrun ,程序结束。