Integer的缓存模式,自动拆装箱,关于128!=128的问题原创
**本文首发于公众号【看点代码再上班】,建议关注公众号,及时阅读最新文章。**
一定要读的原文:
大家好,我是tin,这是我的第19篇原创文章
我们都遇到过Integer a=128,Integer b=128,但a==b不成立
的困惑,今天结合源码和Java的拆装箱说一说其中的原由,先上一个目录:
一、值相等的Integer,==却不成立?
有两个变量a和b,对应分别赋相同的值,但不同情况下的赋值,==
号的比较得到的结果却是截然不同。
package com.tin.example.lang;
/**
* title: IntegerTest
* <p>
* description: Integer包装类相关测试
*
* @author tin @看点代码再上班 on 2021/5/15 下午1:28
*/
public class IntegerTest {
public static void main(String[] args) throws InterruptedException {
Integer a = 127;
Integer b = 127;
System.out.println("res:" + (a == b));
a = 128;
b = 128;
System.out.println("res:" + (a == b));
a = -128;
b = -128;
System.out.println("res:" + (a == b));
a = -129;
b = -129;
System.out.println("res:" + (a == b));
}
}
@示例代码1
当我们运行以上示例代码,输出结果是这样的:
看到这,我们应该都会觉得奇怪,为什么有些情况下相等,有些情况就不相等了?
我们都知道,==
比较的是对象地址引用,不相等说明对象所在的内存地址不一样。
试猜想:相等是因为指向同一个对象,不相等是因为指向不同的对象。
那么,为什么有些值指向同一个对象,有些值却没有指向同一个对象?
要知道真正原因,一看源码便知,源码底下无秘密。
首先,我们要看的是java.lang.Integer#valueOf(int)
方法的源码,其实现如下:
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #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.
*
* @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);
}
API描述得很清楚了,这个方法始终缓存-128~127
之间的值,且,还有可能缓存此范围以外的值。
我们来理解这句话后面的真正原理:
1、始终缓存-128~127之间的值
直接打开java.lang.Integer.IntegerCache
的源码。
java.lang.Integer.IntegerCache
是Integer的一个内部类,它唯一的作用就是实现本地缓存,其内部有一个static代码块:
其义就在于JVM启动的时候,把-128~127
之间的整数放到cache内。
同时也可以看到,放到cache内的都是new Integer()得到的对象。
所以,valueOf方法中IntegerCache.cache[i + (-IntegerCache.low)]
的写法也就是读取缓存了,缓存内相同int值对应的是同一个缓存对象。
以上示例中的a=127
和b=127
都能从缓存读取,且读取到的是同一个对象,==
比较的结果是true没问题。
这是由源码决定的,-128~127之间的整数已被缓存起来,超过这个范围的int值不在缓存内,通过new Integer(i)实例化,这时==
比较的结果自然就是false。
2、还有可能缓存-128~127以外的值
我们可以自定义IntegerCache缓存值范围的上限,比如我可以把上限改为256,也就是支持缓存-128~256之间的整数。
在启动参数中增加以下代码:
我们再次运行@示例代码1
,得到结果是这样的:
很明显,Integer a = 128和Integer b=128是相等的,==
对比的结果是true。
还可以用另一个VM参数设置,达到同样的效果,如下:
支持这样的设置在源码中也有对应的体现:
首先取自定义的值,如果没有自定义则默认127,但如果设置的值小于127,默认也是127,也即,上限最小支持127。
二、自动拆/装箱
什么是自动装箱和拆箱?
❝装箱就是Java自动将基本类型转换成对应的包装类型,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将包装类型转换成基本类型的过程就叫做拆箱,比如将Integer对象转换成int类型值。因为这里的装箱和拆箱是Java内部机制自动完成,所以就称作为自动装箱和拆箱。
基本类型byte, short, char, int, long, float, double 和 boolean 对应的封装类为Byte, Short, Character, Integer, Long, Float, Double, Boolean,它们都有自动拆装箱的过程。
❞
2.1 自动装箱
Integer a = 127
语句没有使用到java.lang.Integer#valueOf(int)
,为什么我却去看这个方法的源码?
原因很简单,Integer a = 127并不是等同于new Integer(127),而是等同于Integer.v
alueOf(127),其中就涉及到Java的自动装箱原理。
关于Java是什么时候做装箱的问题,我们需要打开查看class的汇编指令代码。
执行以下命令:
javac IntegerTest.java
javap -c IntegerTest
得到以下编码:
Compiled from "IntegerTest.java"
public class com.tin.example.lang.IntegerTest {
public com.tin.example.lang.IntegerTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]) throws java.lang.InterruptedException;
Code:
0: bipush 127
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: astore_1
6: bipush 127
8: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
11: astore_2
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: aload_1
16: aload_2
17: if_acmpne 24
20: iconst_1
21: goto 25
24: iconst_0
25: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Z)Ljava/lang/String;
30: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: sipush 128
36: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
39: astore_1
40: sipush 128
43: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
46: astore_2
47: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
50: aload_1
51: aload_2
52: if_acmpne 59
55: iconst_1
56: goto 60
59: iconst_0
60: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Z)Ljava/lang/String;
65: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
68: bipush -128
70: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
73: astore_1
74: bipush -128
76: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
79: astore_2
80: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
83: aload_1
84: aload_2
85: if_acmpne 92
88: iconst_1
89: goto 93
92: iconst_0
93: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Z)Ljava/lang/String;
98: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
101: sipush -129
104: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
107: astore_1
108: sipush -129
111: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
114: astore_2
115: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
118: aload_1
119: aload_2
120: if_acmpne 127
123: iconst_1
124: goto 128
127: iconst_0
128: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(Z)Ljava/lang/String;
133: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
136: return
}
两个关键命令语句解释如下:
-
bipush 127:
表示当 int 取值 -128~127 时,JVM 把对应常量压入栈中。 -
invokestatic #2:
invokestatic是方法调用的字节码指令,#2表示符号引用在常量池中的索引号,根据这个索引号检索常量表,可以查到最终表示的是一个字符串字面量,例如java/lang/Integer.valueOf:(I)Ljava/lang/Integer,这个就是方法的符号引用。
❝为了方便理解字节码,javap反编译的字节码都会给符号引用加注释,注释标明该符号引用的最终表示值,例如java/lang/Object."<init>":()V。
符号引用(Symbolic References)是一个用来无歧义地标识一个实体(例如方法/字段)的字符串,在运行期它会翻译为直接引用(Direct Reference)。对于方法来说,就是方法的入口地址。
所以,方法调用的本质是根据方法的符号引用确定方法的直接引用(入口地址)
❞
在Java编译的时候就已经决定了Integer a = 127
等同于Integer a = Integer.valueOf(127)
,所以,我们上文中才会分析Integer.valueOf方法的源码。
2.2 自动拆箱
自动拆箱,和装箱刚好相反。
我们写如下demo代码:
Integer b = 500;
int c = 500;
System.out.println("res:" + (b == c));
猜都可以猜到了, b == c
是成立的。
同样的,我们通过反编译java文件,看看最终的汇编指令是如何的:
在执行到b == c时候,Java把Integer包装类型进行了拆箱,b等同于b.intValue()
,java.lang.Integer#intValue
返回值就是基础类型值,源码如下:
所以,b == c
成立。
三、其他基本类型的缓存
除了Integer,其他7种基本类型对应的包装类都有装箱实现,首先看一看各自的valueOf源码:
//boolean原生类型自动装箱成Boolean
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
//byte原生类型自动装箱成Byte
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}
//byte原生类型自动装箱成Byte
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}
//char原生类型自动装箱成Character
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
//int原生类型自动装箱成Integer
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//int原生类型自动装箱成Long
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
//double原生类型自动装箱成Double
public static Double valueOf(double d) {
return new Double(d);
}
//float原生类型自动装箱成Float
public static Float valueOf(float f) {
return new Float(f);
}
从valueOf方法就可以看得出来,除了Boolean、Double和Float没有缓存以外,其余的包装类型都有缓存实现。各个包装类型对应的缓存值范围如下:
基本类型 | 大小 | 包装类型 | 缓存范围 | 是否支持自定义缓存范围 |
---|---|---|---|---|
boolean | 6bit | Bloolean | / | / |
char | 8bit | Character | 0~127 | 否 |
byte | 8bit | Byte | -128~127 | 否 |
short | 16bit | Short | -128~127 | 否 |
int | 32bit | Integer | -128~127 | 支持 |
long | 64bit | Long | -128~127 | 否 |
float | 32bit | Float | / | / |
double | 64bit | Double | / | / |
四、结语
我是tin,一个在努力让自己变得更优秀的普通工程师。自己阅历有限、学识浅薄,如有发现文章不妥之处,非常欢迎加我提出,我一定细心推敲并加以修改。
看到这里请安排个“三连”(分享、点赞、在看)再走吧,坚持创作不容易,不要白嫖,你的正反馈是我坚持输出的最强大动力,谢谢!
最后再附上原文链接: