本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html
![]()
本节继续探讨包装类,主要介绍Integer类,下节介绍Character类,Long与Integer相似,就再也不单独介绍了,其余类基本已经介绍完了,再也不赘述。java
一个简单的Integer还有什么要介绍的呢?它有一些二进制操做,咱们来看一下,另外,咱们也分析一下它的valueOf实现。编程
为何要关心实现代码呢?大部分状况下,确实不用关心,咱们会用它就能够了,咱们主要是为了学习,尤为是其中的二进制操做,二进制是计算机的基础,但代码每每晦涩难懂,咱们但愿对其有一个更为清晰深入的理解。设计模式
咱们先来看按位翻转。数组
Integer有两个静态方法,能够按位进行翻转:浏览器
public static int reverse(int i) public static int reverseBytes(int i) 复制代码
位翻转就是将int当作二进制,左边的位与右边的位进行互换,reverse是按位进行互换,reverseBytes是按byte进行互换。咱们来看个例子:缓存
int a = 0x12345678;
System.out.println(Integer.toBinaryString(a));
int r = Integer.reverse(a);
System.out.println(Integer.toBinaryString(r));
int rb = Integer.reverseBytes(a);
System.out.println(Integer.toHexString(rb));
复制代码
a是整数,用十六进制赋值,首先输出其二进制字符串,接着输出reverse后的二进制,最后输出reverseBytes的十六进制,输出为:安全
10010001101000101011001111000
11110011010100010110001001000
78563412
复制代码
reverseBytes是按字节翻转,78是十六进制表示的一个字节,12也是,因此结果78563412是比较容易理解的。bash
二进制翻转初看是不对的,这是由于输出不是32位,输出时忽略了前面的0,咱们补齐32位再看:微信
00010010001101000101011001111000
00011110011010100010110001001000
复制代码
此次结果就对了。
这两个方法是怎么实现的呢?
来看reverseBytes的代码:(您在掘金APP中可能看到乱码,掘金bug,您能够在手机浏览器中打开,或者使用掘金PC版,或者关注个人公众号"老马说编程")
public static int reverseBytes(int i) {
return ((i >>> 24) ) |
((i >> 8) & 0xFF00) |
((i << 8) & 0xFF0000) |
((i << 24));
}
复制代码
以参数i等于0x12345678为例,咱们来分析执行过程:
i>>>24
无符号右移,最高字节挪到最低位,结果是 0x00000012。
(i>>8) & 0xFF00
,左边第二个字节挪到右边第二个,i>>8
结果是 0x00123456,再进行 & 0xFF00
,保留的是右边第二个字节,结果是0x00003400。
(i << 8) & 0xFF0000
,右边第二个字节挪到左边第二个,i<<8
结果是0x34567800,再进行 & 0xFF0000,保留的是右边第三个字节,结果是0x00560000。
i<<24
,结果是0x78000000,最右字节挪到最左边。
这四个结果再进行或操做|,结果就是0x78563412,这样,经过左移、右移、与和或操做,就达到了字节翻转的目的。
咱们再来看reverse的代码:
public static int reverse(int i) {
// HD, Figure 7-1
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;
}
复制代码
这段代码虽然很短,但很是晦涩,究竟是什么意思呢?
代码第一行是一个注释, "HD, Figure 7-1",这是什么意思呢?HD表示的是一本书,书名为Hacker's Delight,HD是它的缩写,Figure 7-1是书中的图7-1,这本书中,相关内容以下图所示:
能够看出,Integer中reverse的代码就是拷贝了这本书中图7-1的代码,这个代码的解释在图中也说明了,咱们翻译一下。
高效实现位翻转的基本思路,首先交换相邻的单一位,而后以两位为一组,再交换相邻的位,接着是四位一组交换、而后是八位、十六位,十六位以后就完成了。这个思路不只适用于二进制,十进制也是适用的,为便于理解,咱们看个十进制的例子,好比对数字12345678进行翻转,
第一轮,相邻单一数字进行互换,结果为:
21 43 65 87
第二轮,以两个数字为一组交换相邻的,结果为:
43 21 87 65
第三轮,以四个数字为一组交换相邻的,结果为:
8765 4321
翻转完成。
对十进制而言,这个效率并不高,但对于二进制,倒是高效的,由于二进制能够在一条指令中交换多个相邻位。
这行代码就是对相邻单一位进行互换:
x = (x & 0x55555555) << 1 | (x & 0xAAAAAAAA) >>> 1;
复制代码
5的二进制是0101,0x55555555的二进制表示是:
01010101010101010101010101010101
复制代码
x & 0x55555555
就是取x的奇数位。
A的二进制是1010,0xAAAAAAAA的二进制表示是:
10101010101010101010101010101010
复制代码
x & 0xAAAAAAAA
就是取x的偶数位。
(x & 0x55555555) << 1 | (x & 0xAAAAAAAA) >>> 1;
复制代码
表示的就是x的奇数位向左移,偶数位向右移,而后经过|合并,达到相邻位互换的目的。这段代码能够有个小的优化,只使用一个常量0x55555555,后半部分先移位再进行与操做,变为:
(i & 0x55555555) << 1 | (i >>> 1) & 0x55555555;
复制代码
同理,以下代码就是以两位为一组,对相邻位进行互换:
i = (i & 0x33333333) << 2 | (i & 0xCCCCCCCC)>>>2;
复制代码
3的二进制是0011,0x33333333的二进制表示是:
00110011001100110011001100110011
复制代码
x & 0x33333333
就是取x以两位为一组的低半部分。
C的二进制是1100,0xCCCCCCCC的二进制表示是:
11001100110011001100110011001100
复制代码
x & 0xCCCCCCCC
就是取x以两位为一组的高半部分。
(i & 0x33333333) << 2 | (i & 0xCCCCCCCC)>>>2;
复制代码
表示的就是x以两位为一组,低半部分向高位移,高半部分向低位移,而后经过|合并,达到交换的目的。一样,能够去掉常量0xCCCCCCCC,代码能够优化为:
(i & 0x33333333) << 2 | (i >>> 2) & 0x33333333;
复制代码
同理,下面代码就是以四位为一组,进行交换。
i = (i & 0x0f0f0f0f) << 4 | (i >>> 4) & 0x0f0f0f0f;
复制代码
到以八位为单位交换时,就是字节翻转了,能够写为以下更直接的形式,代码和reverseBytes基本彻底同样。
i = (i << 24) | ((i & 0xff00) << 8) |
((i >>> 8) & 0xff00) | (i >>> 24);
复制代码
reverse代码为何要写的这么晦涩呢?或者说不能用更容易理解的方式写吗?好比说,实现翻转,一种常见的思路是,第一个和最后一个交换,第二个和倒数第二个交换,直到中间两个交换完成。若是数据不是二进制位,这个思路是好的,但对于二进制位,这个效率比较低。
CPU指令并不能高效的操做单个位,它操做的最小数据单位通常是32位(32位机器),另外,CPU能够高效的实现移位和逻辑运算,但加减乘除则比较慢。
reverse是在充分利用CPU的这些特性,并行高效的进行相邻位的交换,也能够经过其余更容易理解的方式实现相同功能,但很难比这个代码更高效。
Integer有两个静态方法能够进行循环移位:
public static int rotateLeft(int i, int distance) public static int rotateRight(int i, int distance) 复制代码
rotateLeft是循环左移,rotateRight是循环右移,distance是移动的位数,所谓循环移位,是相对于普通的移位而言的,普通移位,好比左移2位,原来的最高两位就没有了,右边会补0,而若是是循环左移两位,则原来的最高两位会移到最右边,就像一个左右相接的环同样。咱们来看个例子:
int a = 0x12345678;
int b = Integer.rotateLeft(a, 8);
System.out.println(Integer.toHexString(b));
int c = Integer.rotateRight(a, 8);
System.out.println(Integer.toHexString(c))
复制代码
b是a循环左移8位的结果,c是a循环右移8位的结果,因此输出为:
34567812
78123456
复制代码
这两个函数的实现代码为:
public static int rotateLeft(int i, int distance) {
return (i << distance) | (i >>> -distance);
}
public static int rotateRight(int i, int distance) {
return (i >>> distance) | (i << -distance);
}
复制代码
这两个函数中使人费解的是负数,若是distance是8,那 i>>>-8是什么意思呢?其实,实际的移位个数不是后面的直接数字,而是直接数字的最低5位的值,或者说是直接数字 & 0x1f的结果。之因此这样,是由于5位最大表示31,移位超过31位对int整数是无效的。
理解了移动负数位的含义,咱们就比较容易上面这段代码了,好比说,-8的二进制表示是:
11111111111111111111111111111000
复制代码
其最低5位是11000,十进制就是24,因此i>>>-8
就是i>>>24
,i<<8 | i>>>24
就是循环左移8位。
上面代码中,i>>>-distance
就是 i>>>(32-distance)
,i<<-distance
就是i<<(32-distance)
。
Integer中还有其余一些位操做,包括:
public static int signum(int i) 复制代码
查看符号位,正数返回1,负数返回-1,0返回0
public static int lowestOneBit(int i) 复制代码
找从右边数第一个1的位置,该位保持不变,其余位设为0,返回这个整数。好比对于3,二进制为11,二进制结果是01,十进制就是1,对于20,二进制是10100,结果就是00100,十进制就是4。
public static int highestOneBit(int i) 复制代码
找从左边数第一个1的位置,该位保持不变,其余位设为0,返回这个整数。
public static int bitCount(int i) 复制代码
找二进制表示中1的个数。好比20,二进制是10100,1的个数是2。
public static int numberOfLeadingZeros(int i) 复制代码
左边开头连续为0的个数。好比20,二进制是10100,左边有27个0。
public static int numberOfTrailingZeros(int i) 复制代码
右边结尾连续为0的个数。好比20,二进制是10100,右边有两个0。
关于其实现代码,都有注释指向Hacker's Delight这本书的相关章节,本文就再也不赘述了。
上节咱们提到,建立包装类对象时,可使用静态的valueOf方法,也能够直接使用new,但建议使用valueOf,为何呢?咱们来看valueOf的代码:
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
复制代码
它使用了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) {
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);
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
private IntegerCache() {}
}
复制代码
IntegerCache表示Integer缓存,其中的cache变量是一个静态Integer数组,在静态初始化代码块中被初始化,默认状况下,保存了从-128到127,共256个整数对应的Integer对象。
在valueOf代码中,若是数值位于被缓存的范围,即默认-128到127,则直接从IntegerCache中获取已预先建立的Integer对象,只有不在缓存范围时,才经过new建立对象。
经过共享经常使用对象,能够节省内存空间,因为Integer是不可变的,因此缓存的对象能够安全的被共享。Boolean/Byte/Short/Long/Character都有相似的实现。这种共享经常使用对象的思路,是一种常见的设计思路,在<设计模式>这本著做中,它被赋予了一个名字,叫享元模式,英文叫Flyweight,即共享的轻量级元素。
本节介绍了Integer中的一些位操做,位操做代码比较晦涩,但性能比较高,咱们详细解释了其中的一些代码,若是但愿有更多的了解,能够根据注释,查看Hacker's Delight这本书。咱们同时介绍了valueOf的实现,介绍了享元模式。
下一节,让咱们来探讨Character。
未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。