java代码最终要转换成机器码执行,机器码都是01的表示(二进制),所以理论上来说二进制的操作是最快的。如果你经常看源码的话,肯定经常能看到java源码中有很多位移操作。
但是,现代jvm确实做了太多的优化,使得这种从计算机一开始就已经有的理论显得没那么重要了。。 看测试代码:
private static void test(){ int max = 1000000; int j = 0; long start = System.currentTimeMillis(); for(int i = 0; i < max; i++){ j = i * 2; } long end1 = System.currentTimeMillis(); for(int i = 0; i < max; i++){ j = i >> 1; } long end2 = System.currentTimeMillis(); for(int i = 0; i < max; i++){ j = i + i; } long end3 = System.currentTimeMillis(); for(int i = 0; i < max; i++){ j = i * 16; } long end4 = System.currentTimeMillis(); for(int i = 0; i < max; i++){ j = i << 4; } long end5 = System.currentTimeMillis(); for(int i = 0; i < max; i++){ j = i + i + i + i + i + i + i + i + i + i + i + i + i + i + i + i; } long end6 = System.currentTimeMillis(); System.out.println("第一个测试,两倍:---------------------"); System.out.println("乘法消耗时间为:\t"+(end1 - start)); System.out.println("位移消耗时间为:\t"+(end2 - end1)); System.out.println("加法消耗时间为:\t"+(end3 - end2)); System.out.println("第一个测试,十六倍:------------------"); System.out.println("乘法消耗时间为:\t"+(end4 - end3)); System.out.println("位移消耗时间为:\t"+(end5 - end4)); System.out.println("加法消耗时间为:\t"+(end6 - end5)); }
分别模拟1000000万次乘法,加法和位移操作,然后看他们执行的时间。执行结果:
第一个测试,两倍:--------------------- 乘法消耗时间为: 12 位移消耗时间为: 2 加法消耗时间为: 3 第一个测试,十六倍:------------------ 乘法消耗时间为: 3 位移消耗时间为: 3 加法消耗时间为: 5
可以看到位移确实是最快的,但是*16明显比*2快很多,而且100万次的位移操作,才比相应的加法和乘法快上几个毫秒,几乎可以忽略不计了。。。
综上,可以认为: 位移操作在现代jvm面前,是几乎没有时间优势的。
不过位移操作还是很方便的。
在java中,位操作符主要包含下面四个:
按位与&, 按位或|, 按位异或^, 按位取反~, 它们的运算规则为:
& 两位全1得1,其他得0
| 两位全0得0,其他得1
^ 两位一位0,一位1得1,其他得0
~ 一位运算符,取反。
位的移位操作包括三个操作符:
算数右移>> 低位溢出,符号位不变(最高位,负1正0),符号位补溢出的高位。
比如,-2在java中保存为补码,111。。10,如果向右移1位,那么除符号位外其他31位向右移1位,变成1_111。。。11然后用符号位,即1补上因为移位溢出的高位,即(_)处,变成111。。。111,即-1。
算数左移<<,符号位不变,其他位向左移,低位不变。
逻辑右移>>>,低位溢出,高位补0。跟算数右移不同的是他高位补0,而不是补符号位。
一个经典的利用位操作和位移操作的算法就是计算int型数i的二进制表示中,1的个数。下面两个方法来计算:
/** * getOneSize: 获取二进制i中1的个数----初始化一个数x=1,然后令i & x,如果结果为ux,说明i第最后一位是1,然后x向左移一位,再次执行该操作。 * @param i * @return * int 返回类型 */ private static int getOneSize(int i){ int x = 0X1; int size = 0; for(int j = 0; j < 32; j++){ if((x & i) == x){ size++; } x = x << 1; } return size; }
/** * getOneSize2: 获取二进制i中1的个数:i和1进行&操作,结果为1说明i的最后一位是1,然后i做逻辑右移一位的操作,直到i变成0为止。 * @param i * @return * int 返回类型 */ private static int getOneSize2(int i){ int x = 0X1; int size = 0; while(i != 0){ size += i & x; i = i >>> 1; } return size; }
测试代码:
System.out.println(getOneSize(123123)); System.out.println(getOneSize2(123123));
结果:
10 10
第二种方法比第一种方法要快,因为第一种方法是需要比对32位的,第二种只需要比对数字i的最高位为1的位数。
然后,今天在测试的时候发现一个比较好玩的现象:
System.out.println(0xFFFFFFFF >>> 31 >>> 1); System.out.println(0xFFFFFFFF >>> 32);
有兴趣的童鞋可以回去自己执行一下试试。这两行执行结果是不一样的,这是因为jvm对位移操作的优化所至。