知识点
- 自动装箱和拆箱
- IntegerCache机制
- toString()实现算法优化
从一道面试题开始
Integer a = 100;
int b = 100;
if (a == b) {
System.out.println("a == b");
} else {
System.out.println("a != b");
}
聪明的你应该马上可以知道答案了,输出就是 a == b
。
自动装箱和自动拆箱
那你可以说说自动装箱和拆箱是怎么回事吗?
把代码编程成字节码,很容易就可以看出来。在这里推荐Compiler Explorer
这个小工具,在浏览器直接搜索就可以了。
当代码中声明Integer a = 100;
实际上在编译成的时候会自动转换成 Integer a = Integer.valueOf(100);
这个过程就是自动装箱。
当Integer类型和基础类型int在比较的时候,Integer类型会自动转换为int类型。a == b
实际上编译成 a.intValue() == b
, 这个过程就是自动拆箱。 这个过程就是自动拆箱。
在上面的截图中,可以看过这个过程。
Integer.valueof() 方法
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在自动装箱过程中,会调用Integer.valueOf(
)方法,首先会检查需要变量i在不在IntegerCach
e范围内,如果有直接返回,没有则new
一个出来。
Integer.intValue()
public int intValue() { return value; }
该方法非常简单,直接返回value。
IntegerCache
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
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);
// Maximum array size is 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;
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() {}
}
上述代码的实现逻辑比较简单:
1、比较IntegerCache
的取值范围,low是-128,high是127,其中high可以通过修改系统变量去设置。
2、创建一个cache数组,依次遍历创建Integer对象放到数组之中。
接着继续看看下面的程序会输出什么结果
Integer a = 128;
int b = 128;
if (a == b) {
System.out.println("a == b");
} else {
System.out.println("a != b");
}
Integer c = 128;
if (a == c) {
System.out.println("a == c");
}else {
System.out.println("a != c");
}
输出结果
a == b
a != c
解释一下:
a和b在比较的时候,a是Integer类型,b是int类型,a在编译的时候会自动拆箱,实际是 a.intValue() == b
, 两个int之间的比较,实际上比较是数值,所以是相等的。
a和c在时间的时候,a和c都是Integer类型,不会发生自动拆箱,而是两个对象之间的比较,所以是不相等的。
趁热打铁,看看下面这两组
Integer d = 100;
Integer e = 100;
if (d == e) {
System.out.println("d == e");
}else {
System.out.println("d != e");
}
Integer m = new Integer(100);
Integer n = new Integer(100);
if (m == n) {
System.out.println("m == n");
}else {
System.out.println("m != n");
}
输出结果
d == e
m != n
解释
为什么d和e两个对象之间比较,输出结果是相等呢,这个就要说到IntegerCache机制了,在声明d和e的时候,编译时会自动装箱,也就是实际上时 Integer e = Integer.valueOf(100)
. 在默认情况下,IntegerCache的范围在-128-127. 所以d和e时相同的。而m和n强制使用new Integer去初始化变量,m和n返回的是不同的对象,所以是不相等的。
小结
1、自动装箱是发生在声明Integer变量时,且初始化表达式是个常量,会调用Integer.valueOf()
方法,创建对象有IntegerCache
机制。
2、自动拆箱时发生在Integer类型和int类型比较的时候, 会自动调用Integer.intValueOf()
方法。
3、IntegerCache缓存机制默认范围是-128-127。
Integer.toString()实现
public static String toString(int i, int radix) {
if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX)
radix = 10;
/* Use the faster version */
if (radix == 10) {
return toString(i);
}
char buf[] = new char[33];
boolean negative = (i < 0);
int charPos = 32;
if (!negative) {
i = -i;
}
while (i <= -radix) {
buf[charPos--] = digits[-(i % radix)];
i = i / radix;
}
buf[charPos] = digits[-i];
if (negative) {
buf[--charPos] = '-';
}
return new String(buf, charPos, (33 - charPos));
}
这段代码定义了一个方法,用于将长整型(long)数字转换为指定基数(radix)的字符串表示
1、基数检查,只有是2-36进制
2、10进制特殊处理
3、声明一个char数据,为什么是33呢,int类型是32位,加上’-',所以一共是33
4、处理负数
5、while循环,计算余数和商,直到最后一个商小于基数的时候退出循环
6、处理最后一个商
7、处理负数
8、将char数组转换位String对象
public static String toString(int i) {
if (i == Integer.MIN_VALUE)
return "-2147483648";
int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
char[] buf = new char[size];
getChars(i, size, buf);
return new String(buf, true);
}
static void getChars(int i, int index, char[] buf) {
int q, r;
int charPos = index;
char sign = 0;
if (i < 0) {
sign = '-';
i = -i;
}
// Generate two digits per iteration
while (i >= 65536) {
q = i / 100;
// really: r = i - (q * 100);
r = i - ((q << 6) + (q << 5) + (q << 2));
i = q;
buf [--charPos] = DigitOnes[r];
buf [--charPos] = DigitTens[r];
}
// Fall thru to fast mode for smaller numbers
// assert(i <= 65536, i);
for (;;) {
q = (i * 52429) >>> (16+3);
r = i - ((q << 3) + (q << 1)); // r = i-(q*10) ...
buf [--charPos] = digits [r];
i = q;
if (i == 0) break;
}
if (sign != 0) {
buf [--charPos] = sign;
}
}
在上述代码中,比较引人注意的是有两个小片段
// Generate two digits per iteration
while (i >= 65536) {
q = i / 100;
// really: r = i - (q * 100);
r = i - ((q << 6) + (q << 5) + (q << 2));
i = q;
buf [--charPos] = DigitOnes[r];
buf [--charPos] = DigitTens[r];
}
这里使用了两个技巧。
第一个不再是用除以10去计算商,而是使用除以100,一次循环中计算两次
第二个是计算余数,不是使用%运算,也不是 r = i - (q * 100)
; 而是使用 r = i - ((q << 6) + (q << 5) + (q << 2))
;
这里没有使用乘法,而是使用左移和加法代替。
让我们逐步解释这个表达式:
q << 6:这是将 q 左移 6 位,相当于 q 乘以 (2^6 = 64)。
q << 5:这是将 q 左移 5 位,相当于 q 乘以 (2^5 = 32)。
q << 2:这是将 q 左移 2 位,相当于 q 乘以 (2^2 = 4)。
这三个位移操作分别模拟了 q 乘以 64、32 和 4。加在一起,它们就模拟了 q 乘以 100(因为 (64 + 32 + 4 = 100))。
再来看另外一个片段
for (;;) {
q = (i * 52429) >>> (16+3);
r = i - ((q << 3) + (q << 1)); // r = i-(q*10) ...
buf [--charPos] = digits [r];
i = q;
if (i == 0) break;
}
首先,在计算商的时候并不是一个常规的除法运算。看到 q = (i * 52429) >>> (16+3);
这里并没有使用除法,代替的是 乘法 和 右移运算。
这行代码计算了i除以10的商。这里使用了52429这个特殊的数。这个数是通过100000 / 10
(即10000除以10,再左移5位)得到的。这样,i * 52429
相当于i乘以10000再左移5位。然后,通过无符号右移操作>>>
来除以(2^19)
(即右移19位),相当于再除以(2^14)
(即65536)。这样,q就得到了i除以10的商的近似值。
由上面两个小片段中可以知道,在库函数级别的代码中,对代码和性能的要求非常高。
总结
1、自动装箱和拆箱 - 是一种语法糖,在编译器就实现。
2、IntegerCache机制 - 一种缓存机制,节约内存,避免对象重复创建。
3、toString()实现算法优化 - 使用位移算法替换乘法运算,使用乘法运算替换除法运算。