以前也曾多次了解过关键位运算,不过一直都不够深入,前段时间看jdk的源码,发现里面关于数字的类实现中使用了大量的位运算,就借着这个机会好好的重温了一下位运算。
首先需要先说一下数据在计算机中的存储,这是一个老生常谈的话题,所以就简单的说一下。
在计算机中一切数据都是以二进制存放的,那么这里只讨论整数,在java中整数被分为4种类型,分别是byte,short,int,long,其中的区别就是位数不同,分别是8,16,32,64位,顾名思义,就是值在计算机中会以几位二进制存储。
那么对于一个整数,它有正数和负数之分,在存储时的区别就是第一位。以8位的byte类型举例:
64的二进制表示就是01000000,由于byte的范围是-128~127,也就是说最大值127的表示为:01111111。
对于负数,在计算机中会先将正数的二进制取反,然后再加一,这样第一位就是1。如:
-64的二进制表示为:11000000,因为64取反为10111111,然后再加一为11000000,而对于一个负数的二进制,将其再转为正数的方法同样是取反再加一,如11000000取反后为00111111,再加一为01000000,这个就是64的二进制。
基于此就可以简答了解一下一个很常见的问题,Integer.MAX_VALUE + 1是多少?
首先需要明白在计算机中一切都是以二进制运行的,所以如同这样的加法也是二进制之间的运算,所以先将Integer.MAX_VALUE转为二进制:01111111111111111111111111111111,将其加一就是10000000000000000000000000000000,由于1是第一位,所以很显然此数是负数,同时剩下的数字都为0,这代表这是Integer的最小值,即Integer.MIN_VALUE,那么第二个问题,将次数再加一,是会回到1还是只是+1呢?继续计算,二进制编程1000000000000000000000000001取反后加一就是Integer.MAX_VALUE,也就是说现在的数字为-Integer.MAX_VALUE,所以在整数计算中,对于一个整数的类型最大值再继续相加就会直接变成这个类型的最小值,然后会以这个最小值为基础继续相加,可以最后做个试验
System.out.println(Integer.MAX_VALUE + Integer.MAX_VALUE)
猜这个结果会是多少?结果是-2.
下面开始介绍java的位运算,java的位运算有以下几种:
~取反:这个取反是指对二进制取反,而不是对值,由上面的介绍可知,负数不仅仅是取反的,所以你可以试验一下
System.out.println(~64);
结果是-65.
&按位与,对二进制的每一位都做与操作,最后得到一个值。
|按位或,同上,做的是或操作。
^按位异或,这个大多数人也是知道的,就是如果操作的两位数是相同的,比如都是1或者都是0,则结果为0,否则为1.
>>右移,我们知道在二进制中,假设用一个32位的Int表示一个64,那么高位就都是0,所以当我们把整个二进制数右移,如0100000 >> 2 = 0001000,可以看到右移两位后的数变成了8,可以分析出其实右移就是一个除以2的操作,对于非2,4,8,16,64的数也可以试验一下:
System.out.println(3 >> 1);
System.out.println(5 >> 1);
System.out.println(63 >> 1);
其结果分别为1,2,31.所以右移就是一个整除2的过程,右移一位就是除一次,n位就是除n次。
同时需要注意的>>是带符号的,也就是说它的高位补充数是由最高位来决定的,正数的最高位为0,负数的最高位为1,所以负数 >>后还是负数。
同上面对应的是>>>无符号右移,原理和上面一样,不同的是它的高位总是由0来补充。那么思考一个问题:-64 >>> 1是多少呢?答案自然不是32,因为-64在计算机中的存在为:11111111111111111111111111000000,那么无符号右移一位就变成了01111111111111111111111111100000,这个值等于2147483616,所以对于这些位运算不要用惯性思维去思考,一定要想明白二进制是如何工作的。
最后的一个位运算是<<,左移,这个是没有<<<的,因为左移后低位肯定是由0来补充的,那么我们总结出>>相当于除2,<<呢?继续做个试验:
System.out.println(-64 << 1);
System.out.println(64 << 1);
System.out.println(25 << 1);
结果是-128,128,50,显然是乘2的。
最后介绍一个jdk源码中对进制转换应用的方法:
首先思考当你需要将一个10进制的数转为其他进制时的做法,我们不断用需要转换的进制去除这个10进制数,然后将余数倒过来。
那么思考如何使用位运算来实现呢?首先看一下jdk中的源码:
/**
* Convert the integer to an unsigned number.
*/
private static String toUnsignedString(int i, int shift) {
char[] buf = new char[32];
int charPos = 32;
int radix = 1 << shift;
int mask = radix – 1;
do {
buf[--charPos] = digits[i & mask];
i >>>= shift;
} while (i != 0);return new String(buf, charPos, (32 – charPos));
}
其中的数组digits里面存放的是0-9和a-z用于方便数值转换。
其核心就是两处:i & mask 和 i >>>= shift,从这个方法名中可以得知这是一个无符号转换的方法,所以使用了>>>。
思考一下这两个操作都有什么作用呢?首先shift这个参数代表要转换的进制,1代表2进制,3代表8进制,4代表16进制,而看代码radix = 1 << shift也可以得知radix会通过位运算转为正确的进制。
那么mask = radix – 1是为什么呢?假设我们要转换16进制,那么radix = 16,而mask = 15,二进制表示为00000000000000000000000000001111,那么i & mask的值是否可以想象为i % 16呢? 而i >>> = shift,在此处为 i >>>= 4,其实就相当于是i / 16。