一. 基本描述
java.lang.Integer
是最常用的Java类型之一,然而恐怕很少有人会去注意这个支撑起整个Java世界的小小基石。
Integer
类的声明是 public final class Integer extends Number implments Comparable
,我们可以发现Integer
类是不能被继承的。
Integer
的核心字段是private final int value
,这个字段代表着Integer
对象所表示的整数值,final
修饰符意味着Integer
是immutable
即不可变的。
还有一个有趣的地方在Integer
的哈希方法
@Override
public int hashCode() {
return Integer.hashCode(value);
}
public static int hashCode(int value) {
return value;
}
Integer
的哈希方法直接返回了整数表示
二. reverse方法
reverse方法的实现很有意思
public static int reverse(int i) {
i = (i & 0x55555555) << 1 | (i >>> 1) & 0x55555555;
i = (i & 0x33333333) << 2 | (i >>> 2) & 0x33333333;
i = (i & 0x0f0f0f0f) << 4 | (i >>> 4) & 0x0f0f0f0f;
i = (i << 24) | ((i & 0xff00) << 8) |
((i >>> 8) & 0xff00) | (i >>> 24);
return i;
}
咱们一步步分析,首先是第一行i = (i & 0x55555555) << 1 | (i >>> 1) & 0x55555555
注意了啊,很多人对16进制不敏感,16进制的0x5
展开成二进制就是0101
,0x55555555
就是01010101010101010101010101010101
,那么代码前半部分(i & 0x55555555) << 1
的功能是只保留整数i的偶数位(从0开始计数)并左移一位,代码后半部分(i >>> 1) & 0x55555555
的功能是只保留整数i的奇数位
第一行代码合起来所完成的功能就是将i的二进制每2位划为一组,并将组内的位交换位置。
类似的,0x3
展开成二进制就是0011
,第二行代码的功能就是将i的二进制每4位划为一组,并将组内的前两位和后两位交换位置。
看起来就像这样:
初始 1234 | 5678
第一趟 2143 | 6587
第二趟 4321 | 8765
显然第三行代码的作用就是将二进制每8位划为一组,将组内前四位和后四位交换位置。
最后一行就更好理解了,int
类型在Java里是用4个字节表示的,i << 24)
将i的最后一个字节放到第一个字节的位置,(i & 0xff00) << 8
将i的第三个字节放到第二个字节的位置,(i >>> 8) & 0xff00
将i的第二个字节放到第三个字节的位置,i >>> 24
将i的第一个字节放到最后的位置
三. bitCount方法
bitCount方法的实现很精妙,初看十分难懂,而最难懂的正是第一行
public static int bitCount(int i) {
i = i - ((i >>> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
i = (i + (i >>> 4)) & 0x0f0f0f0f;
i = i + (i >>> 8);
i = i + (i >>> 16);
return i & 0x3f;
}
又是熟悉的0x55555555
,(i >>> 1) & 0x55555555
将i的奇数位剥离出来并右移一位,真正让人难以理解的是这个减法,着实难以理解它的目的。为了讲述方便,我们用4位二进制来展示
假设希望求1101
的1'
位数量,经过第一行代码则变成了1101 - 0100
,注意看啊,假如每2位划分为一组,我们有11 - 01 = 10
和01 - 00 = 01
,最后得到的结果刚好是每2位一组里的1'
位数量,是不是很奇妙11
右移取奇数位得到01
,11 - 01 = 10
即包含2个1'
10
右移取奇数位得到01
,10 - 01 = 01
即包含1个1'
01
右移取奇数位得到00
,01 - 00 = 01
即包含1个1'
00
右移取奇数位得到00
,00 - 00 = 00
即包含0个1'
我是真的服了,这种规律都能发现到。或许这大概率不是JDK作者原创的,但是这个方法真的是让我忍不住喊一句——喵喵!
下面贴个图展示一下第一步
后续的代码都是建立在第一行的基础上的,经过第一行代码后,我们知道将i的二进制每2位划为一组,则这个组里的二进制数就表示了该组的1的数量,后面我们要做的就是将这些数加起来自然就得到整个二进制所包含的1的数量
再聊一下这段代码的细节之处吧,也许各位没注意,但是我注意到,第三行代码是先相加再用掩码清空,第四行和第五行则完全没有用上掩码,这是为什么?
废话不多说,直接上图好了
第四行和第五行不需要掩码的原因是一样的,因为不会造成溢出啊
四. toString()方法
默认的toString()
方法是针对10进制基数做了优化的,Integer
还有个适用性更广的toString(int i, int radix)
方法,当radix = 10
时就会代理给toString()
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);
}
stringSize()
方法蛮有意思,进制基数固定后,低量级的数,它的字面表示的长度比高一个量级的数的长度少一位。比如量级为10的数33,比量级为100的数101的长度少一位。所以stringSize()
方法直接用查表法得到整数i的10进制基数字面表示长度。当然,如果i是负数需要先将其转成正数
final static int [] sizeTable = { 9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE };
static int stringSize(int x) {
for (int i=0; ; i++)
if (x <= sizeTable[i])
return i+1;
}
然后是针对10进制的特别优化
static void getChars(int i, int index, char[] buf) {
int q, r;
int charPos = index;
char sign = 0;
if (i < 0) {
sign = '-';
i = -i;
}
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];
}
for (;;) {
q = (i * 52429) >>> (16+3);
r = i - ((q << 3) + (q << 1));
buf [--charPos] = digits [r];
i = q;
if (i == 0) break;
}
if (sign != 0) {
buf [--charPos] = sign;
}
}
代码里将q * 100
和q * 10
的乘法运算优化成((q << 6) + (q << 5) + (q << 2))
和((q << 3) + (q << 1))
。像这种类似的用移位加法代替乘法的技巧在JDK源码里很常见。DigitOnes
和DigitTens
也是查表法,Integer用数组将0到99的数的个位字符串表示和10位字符串表示存起来,通过空间的浪费换取运算时间的降低。
整处代码之精华自然是在q = (i * 52429) >>> (16+3)
,那么我们需要知道 219 = 524288
,并且52429 >>> 19 = 52429 / 524288
下面是我摘抄自网络的内容
103/1024≈0.1006 (2^10)
205/2048≈0.100098 (2^11)
……
26215/262144≈0.100002 (2^18)
52429/524288≈0.10000038 (2^19)
104858/1048576≈0.10000038 (2^20)
可以看到这里实际上是用乘法代替了除以10的除法
五. IntegerCache
相信大家面试的时候一定有被问到过这种问题:Integer a = 10; Integer b = 10
,a == b 是否成立?
默认情况下Integer
会在加载时将[-128, 127]区间里的数(包含边界)都生成Integer对象缓存起来,如果代码里使用自动装箱的赋值方式,即Integer x = 常量值
的形式,且常量值在[-128, 127]内则会使用缓存中的Integer
对象
源码注释上说缓存的大小可以通过JVM的启动参数-XX:AutoBoxCacheMax=
来修改,我自己尝试了一下,结合源码注释我发现这个参数无法影响到下界,区间下界是写死的-128,而且这个参数修改的是上界,而不是它的名字表达的整个缓存大小,所以实际的缓存大小应当是XX:AutoBoxCacheMax - (-128) + 1