Java的位运算符是指针对二进制数的每一位进行运算的符号,他在运算的时候不关心数据的实际含义(是正数还是负数,等等),而是直接根据数据数据在内存的保存形式来计算的,它是Java基础中的基础,而大部分开发人员对这个不甚了解,所以本文来介绍它的一些基本使用。
文章目录
原码反码补码的概念
在正式介绍位运算符之前,我们先来看看关于反码补码等的相关知识。
正数的原码反码补码相同,都是将数字转换为二进制形式,然后将高位补0。
比如说对于8位来说:
- 10所对应的原码反码补码都是 0000 1010
- 30所对应的原码反码补码都是 0001 1110
而对于负数,负数的原码是它的绝对值对应的二进制,而最高位是1;
所以:
- -10所对应的原码是 1000 1010
- -30所对应的原码是 1001 1110
负数的反码是它原码最高位除外的其余所有位取反;
所以:
- -10所对应的反码是 1111 0101
- -30所对应的反码是 1110 0001
而负数的补码是将其反码的数值+1;
所以:
- -10所对应的补码是 1111 0110
- -30所对应的补码是 1110 0010
是不是感觉负数的三种很杂乱?没关系,马上让你觉得不过如此~
我们对于刚才计算出来的负数的三种码,我们抛开符号为的概念,把三种码作为单存的二进制数来看:
首先对于原码,原码是在最高位写上1得到的,而对于8位二进制,最高位的1是128,所以:
- -10所对应的原码是 1000 1010,而这个二进制数是128+10;
- -30所对应的原码是 1001 1110,而这个二进制数是128+30;
然后对于反码,在原码的基础上,最高位不变,其余各位取反,也就是说抛开最高位,其余各位变化前后的和是111 1111,所以:
- -10所对应的反码是 1111 0101,其值为127-10+128
- -30所对应的反码是 1110 0001,其值为127-30+128
至于补码,就是反码加上1,所以:
- -10所对应的补码是 1111 0110,其值为256-10
- -30所对应的补码是 1110 0010,其值为256-30
计算机为什么要引入补码的概念呢?主要是补码能够简化计算,例如对于8位的二进制数,从0000 0000一直到1111 1111,如果看作补码,所表示的数依次为0,1,…,127,-128,-127,…,-2,-1。
对于两个正数的加法,正数的补码就是它本身,所以直接相加就能得到结果(注意,如果和超过了127,结果会错误);
对于两个负数的加法,我们仍然可以将他们的补码直接相加,而得到结果~比如对于两个负数-a和-b,他们的补码分别是256-a和256-b,相加后是512-(a+b),当a+b小于128时(a+b>128时这两个数相加超过了范围,得不到正确的结果),在计算机的表示是1 xxxx xxxx由于最高位的1超过了8位的范围所以舍去,所以相加结果是128-(a+b)而这就是-a-b的补码;
对于正数a和负数-b之间的加法,我们将其补码相加后得到256+a-b,而这就是a-b的补码(不管ab谁大都成立)。
由于减法可以对被减数取反,所以我们只用考虑加法运算。
所以说如果使用补码,可以很好的将加减法运算转换成两个正数(负数的补码可以看成是一个正数256-x)的加法运算。如果计算机保存补码,在做数之间的加减法运算的时候能够直接将补码相加而得到正确的结果(没超过范围的情况下)。
我们也能够直接相加得到正确的结果。
由于减法可以对被减数取反,所以我们只用考虑加法运算
实际上,考虑将所以有理数按256取模得到的数相等来分成256组,在做加减乘运算的时候,用同组的任意两个数运算得到的结果均是相同的,而负数和其补码是位于同一个组的,所以在做四则运算的时候,加减乘运算完全可以使用其的补码。
Java数字在内存中的存储形式
整型在 Java的内存形式
学习完补码之后,我们认识到如果使用补码,其在做运算时的方便之处。而Java的整形就是保存的各个数的补码,我们也可以在 Java上验证:
System.out.println(Integer.toHexString(-1));
System.out.println(Integer.toHexString(-10));
System.out.println(Integer.toHexString(Integer.MAX_VALUE));
System.out.println(Integer.toHexString(Integer.MAX_VALUE));
System.out.println(Long.toHexString(-1));
System.out.println(Long.toHexString(Long.MAX_VALUE));
ffffffff
fffffff6
7fffffff
7fffffff
ffffffffffffffff
7fffffffffffffff
整型在Java上保存的形式具体如下:
类型 | 所占字节数 | 二进制位数 | 表示的数的范围 | -1的存储形式 |
---|---|---|---|---|
byte | 1 | 8 | -27(-128)~27-1(127) | 0xff |
short | 2 | 16 | -215~215-1 | 0xffff |
int | 4 | 32 | -231~231-1 | 0xffff ffff |
long | 8 | 64 | -263~263-1 | 0xffff ffff ffff ffff |
以上是Java上整型的在内存的保存。
浮点型在 Java的内存形式
对于小数,现代计算机小数的保存标准一般都是IEEE二进制算数标准,在正式介绍这个标准之前,我们先来回忆以前学习的科学计数法,科学计数法上指将一个数表示成一个1~10以内的数乘以10的n次方的形式,例如:
- 120=1.2*102
- -10=-1*101
- 0.023=2.3*10-2
以上是科学计数法在10进制的表现形式。从此我们可以引申出二进制下的科学计数法:
- 10=1.01*1011(左边的数是十进制表示,右边的数都是二进制表示)
- -0.125=-1*10-11
这种表示方式与小数在Java上的保存形式有什么关系呢?IEEE二进制算数标准规定了32位精度的小数保存的形式:
SEEE EEEE EMMM MMMM MMMM MMMM MMMM MMMM
- 其中S代表符号,0为正1为负;
- 8个E代表指数,8为二进制代表的数数从0~255,要减去偏移量127(这样才能表示指数为负数的数),指数的范围是-127~128;
- 剩下的23个M是小数点后面的数,由于小数点前面都是1,所以不需要保存。
那么按照这个标准,10的表示是:符号为0,指数为127+3,也就是1000 0010,位数是0100 0000 …,合起来就是:0 1000 0010 0100 0000 …
同理-0.125是1 0111 1100 00000…
我们也可以使用代码进行验证:
private static void print(float a) {
//Float.floatToIntBits是将float类型的小数按照它的保存形式直接转换为int类型,因为他们都是32位的
StringBuilder s = new StringBuilder(Integer.toBinaryString(Float.floatToIntBits(a)));
for (int i = 0; i < 32 - s.length(); i++) {
//按二进制输出的时候,如果高位是0,会被省略,用这个方法来补全到32位
s.insert(0, "0");
}
System.out.println("sign" + s.substring(0, 1));
System.out.println("Exponent" + s.substring(1, 9));
System.out.println("Significand" + s.substring(9));
}
print(10);
print(-0.125f);
sign0
Exponent10000010
Significand01000000000000000000000
sign1
Exponent01111100
Significand00000000000000000000000
对于双进度(64位具体到 Java上就是double类型)浮点数,规则与上面类似,分为1个符号位,11个指数位,52个尾数位,指数位的偏移是1023,对于10,他就是0 1000 0000 010 0100 0000 …
代码验证如下:
System.out.println(Long.toBinaryString(Double.doubleToLongBits(10)));
100000000100100000000000000000000000000000000000000000000000000//最高位的0被省略了
关于float的最大值,我们想到的肯定是符号为是0,指数域取最大全是1,减去偏移后是128,尾数全部是1,这个数对应与int的最大值,我们可以用如下方法打印出来:
System.out.println(Float.intBitsToFloat(Integer.MAX_VALUE));
NaN
结果打印出来发现是NaN,无意义,什么鬼??其实,IEEE对指数域是0,255的数做了特殊的规定:
形式 | 指数 | 小数部分 |
---|---|---|
零 | 0 | 0 |
非规约形式 | 0 | 非0 |
规约形式 | 1到254 | 任意 |
无穷 | 255 | 0 |
NaN | 255 | 非零 |
其中非规约形式的指数部分全是0,正常应该是视指数部分为-127,但是非规约形式规定:指数部分当作-126,相应的尾数部分并不在前面补1,例如对于如下串:1 0000 0000 1000…它的指数部分是0,小数部分非0,所以按照非规约形式解析,其值为:-0.5x2-126
用代码验证如下:
System.out.println(Float.intBitsToFloat(0b1_0000_0000_10000000000000000000000));
System.out.println(0.5*Math.pow(2,-126));
-5.877472E-39
5.8774717541114375E-39
可以看出他们在数值上大致相等(计算会有精度损失)。
对于32位精度的数,可以得出如下表:
类别 | 正负号 | 实际指数 | 有偏移指数 | 指数域 | 尾数域 | 数值(括号里的代表2进制) |
---|---|---|---|---|---|---|
零 | 0 | -127 | 0 | 0000 0000 | 000 0000 0000 0000 0000 0000 | 0.0 |
负零 | 1 | -127 | 0 | 0000 0000 | 000 0000 0000 0000 0000 0000 | −0.0 |
1 | 0 | 0 | 127 | 0111 1111 | 000 0000 0000 0000 0000 0000 | 1.0 |
-1 | 1 | 0 | 127 | 0111 1111 | 000 0000 0000 0000 0000 0000 | −1.0 |
绝对值最小的非规约数 | * | -126 | 0 | 0000 0000 | 000 0000 0000 0000 0000 0001 | ±(0.000…1)× 2-126 |
中间大小的非规约数 | * | -126 | 0 | 0000 0000 | 100 0000 0000 0000 0000 0000 | ±(0.1)x2-126 |
最大的非规约数 | * | -126 | 0 | 0000 0000 | 111 1111 1111 1111 1111 1111 | ±(0.11…1) × 2-126 |
最小的规约数 | * | -126 | 1 | 0000 0001 | 000 0000 0000 0000 0000 0000 | ±(1.000…0)x2-126 |
绝对值最大的规约数 | * | 127 | 254 | 1111 1110 | 111 1111 1111 1111 1111 1111 | ±(1.111…1) × 2127 |
正无穷 | 0 | 128 | 255 | 1111 1111 | 000 0000 0000 0000 0000 0000 | +∞ |
负无穷 | 1 | 128 | 255 | 1111 1111 | 000 0000 0000 0000 0000 0000 | −∞ |
NaN | * | 128 | 255 | 1111 1111 | non zero | NaN |
可以看出绝对值最大的非规约数大概是绝对值最小的规约数的一般,由此也可以看出非规约数的定义主要是为了能表示绝对值很小的数。
float和double的精度问题
由于计算机保存的数是基于二进制的,而我们习惯对数的表示数十进制的,所以会有很多我们习惯于的数在二进制下并不是有限小数,这也就导致使用float或者double表示浮点数的时候会有精度的丢失,比如对于十进制数0.1,它转换为二进制数是0.0 0011 0011 0011…,显然,我们使用有限位的二进制是不能够精确表示0.1的,而这会导致一些奇奇怪怪的问题:
System.out.println(1.0 - 0.42);
System.out.println(0.05 + 0.01 == 0.06);
System.out.println((int) (4.015 * 100 + 0.5));
0.5800000000000001//预期输出0.58
false//预期输出true
401//预期输出402
这些问题我们可以通过技术手段来规避,在我们判断浮点数是否相等的时候,我们一般根据当前的业务所涉及到的数据,选择一个相对业务而言很小的数,如果两个数相减小于这个数我们就认为两个数相等,而将double转为int的时候,可以根据业务的需求适当的选择是强转还是使用四舍五入,例如对于上面的例子,我们可以是用如下方法来规避精度问题:
System.out.println(Math.abs(0.05 + 0.01 - 0.06) < 1e-5);
System.out.println(Math.round(4.015 * 100 + 0.5));
true
402
那么对于int和double而言,精度的损失大概是多少呢?对于规约数而言,它表示数的部分有23位,加上默认的1,损失的精度不大于数本身的1/223,实际上,由于最低位是四舍五入得到的,精度损失最多为1/224,而双精度数是1/253。
对这两个数以十为底取对数后,能得出单精和双精浮点数可以保证7位和15位十进制有效数字。
Java中的位运算符
Java共有如下几种位运算符:
符号 | 作用 | 例子 |
---|---|---|
& | 求与 | 1&1=0 1&0=0(对于某一位的计算) |
| | 求或 | 1|0=1 0|0=0(对于某一位的计算) |
^ | 求异或 | 1^1=0 0^1=1(对于某一位的计算) |
~ | 求非 | ~1=0 ~0=1(对于某一位的计算) |
<< | 左移 | 1 << 1 =2(对于int数的计算) |
>> | 右移 | 4 >> 1 =2(对于int数的计算) |
>>> | 无符号右移 | 4 >>> 1=2(对于int数的计算) |
左移右移无符号右移的具体含义可以参考如下例子:
System.out.println(Integer.toHexString(0x7fffffff >> 4));
System.out.println(Integer.toHexString(0x8fffffff >> 4));
System.out.println(Integer.toHexString(0xffffffff >> 4));
System.out.println(Integer.toHexString(0x0fffffff >> 4));
System.out.println();
System.out.println(Integer.toHexString(0x7fffffff << 4));
System.out.println(Integer.toHexString(0x8fffffff << 4));
System.out.println(Integer.toHexString(0xffffffff << 4));
System.out.println(Integer.toHexString(0x0fffffff << 4));
System.out.println();
System.out.println(Integer.toHexString(0x7fffffff >>> 4));
System.out.println(Integer.toHexString(0x8fffffff >>> 4));
System.out.println(Integer.toHexString(0xffffffff >>> 4));
System.out.println(Integer.toHexString(0x0fffffff >>> 4));
7ffffff
f8ffffff
ffffffff
ffffff
fffffff0
fffffff0
fffffff0
fffffff0
7ffffff
8ffffff
fffffff
ffffff
总结来说:
-
右移:整体向右移,原来的最高位是0,就补0,原来的最高位是1,就补1;
-
左移:整体左移,在最低位补0;
-
无符号右移:最高位补0。
位运算符的使用又如下几点需要注意:
- 位运算符都是在整型保存在内存地址上的每一位进行运算的,所以有些会看起来超出我们的常识;
- 左移右移并不能简单的看作是在数的基础上乘以2或者除以2,而要结合它的内存形式来具体分析。
例如:
System.out.println(~1);
结果是-2而不是0
这是因为1在内存上是000....001,~1就是111...1110,而这是-2的补码,所以结果是-1;
System.out.println(-1 >> 1);
结果还是-1而不是0
这是因为-1在内存上是111...1,它的符号位是1,所以右移的时候高位补1,结果不变;
System.out.println(Integer.MAX_VALUE << 1);
结果是-2,正数左移移成了负数。。。
System.out.println(0xBFFFFFFF);
System.out.println(0xBFFFFFFF << 1);
-1073741825
2147483646
负数左移成了正数。。。
System.out.println(-1 >>> 1);
2147483647
-1左移成了int的最大值。。。
Java位运算符的使用
我们来看一项位运算符的具体使用:处理颜色值。
在Android中,颜色值是用int类型的数来表示的,例如0xffffffff表示白色,其中前两位代表透明度,后六位分别代表红绿蓝三色的值,注意0xffffff表示的颜色是透明的,因为它没有写明透明度,而这个int值对应的最高两位是00,所以它代表的颜色是透明的。
现在我们来考虑一个场景:取一个颜色ARGB的值,这在颜色渐变的动画中要使用到(因为如果想要颜色平滑的渐变,必须要在ARGB上分别渐变,如果按照int值的简单渐变,会导致颜色的闪动),比如对于颜色0xff123456,我们想分别取到ff,12,34,56,那么会很容易写出如下代码:
//取B 56
System.out.println(Integer.toHexString(0xff123456 % 0xff));
//取G 34
System.out.println(Integer.toHexString(0xff123456 / 0xff % 0xff));
//取R 12
System.out.println(Integer.toHexString(0xff123456 / 0xffff % 0xff));
//取A ff
System.out.println(Integer.toHexString(0xff123456 / 0xffffff));
ffffff9c
ffffff57
ffffff13
0
结果完全和我们想的不一样,实际上,颜色0xff123456表示的int数是一个负数因为它的最高位是1,他已经超过了int的最大正数的范畴,我们要正确的得到ARGB的方法是使用位运算符:
//取B 56
System.out.println(Integer.toHexString((0xff123456 & 0xff) >>> 0));
//取G 34
System.out.println(Integer.toHexString((0xff123456 & 0xff00) >>> 8));
//取R 12
System.out.println(Integer.toHexString((0xff123456 & 0xff0000) >>> 16));
//取A ff
System.out.println(Integer.toHexString((0xff123456 & 0xff000000) >>> 24));
56
34
12
ff
先通过与运算符拿到ARGB对应的值,然后在通过无符号右移将值移到最低位,之后可以分别改变ARGB的值,然后在合并成颜色,读者可自行尝试。
最后,我们来尝试使用位运算符来实现加减乘除运算。
我们先来实现求相反数。由于正数的补码和他相反数的补码的关系我们已经很清楚,那么我们只需要对正数和负数分别处理就好了:
public static int getOppositeNumber(int number) {
if ((number & 0x80000000) == 0x80000000) {
// number是负数
// 先把这个数看作一个无符号的数,
//执行-1,方法是从最低位算起,
//如果遇到0,就将之变成1
//如果遇到1,就将之变成0,并跳出循环
for (int i = 0; i <= 31; i++) {
// System.out.println(number);
int flag = 1 << i;
if ((number & flag) == flag) {
// 当前位是1,就换成0
number = number ^ flag;
break;
} else {
number = number ^ flag;
}
}
// System.out.println(number);
// 执行全部位的取反
return number ^ 0xffffffff;
} else {
// number是正数
// 执行取反
number=number ^ 0xffffffff;
// System.out.println(number);
// 执行+1,方法和执行-1差不多
for (int i = 0; i <= 31; i++) {
// System.out.println(number);
int flag = 1 << i;
if ((number & flag) != flag) {
// 当前位是0,就换成1
number = number ^ flag;
break;
} else {
number = number ^ flag;
}
}
return number;
}
}
public static void main(String[] args) {
System.out.println(getOppositeNumber(-10));
System.out.println(getOppositeNumber(-12423));
System.out.println(Integer.toHexString(getOppositeNumber(Integer.MIN_VALUE)));
System.out.println(getOppositeNumber(10));
System.out.println(getOppositeNumber(2346760));
System.out.println(Integer.toHexString(getOppositeNumber(Integer.MAX_VALUE)));
System.out.println(getOppositeNumber(0));
}
10
12423
80000000
-10
-2346760
80000001
0
其中int的最小值的相反数算出来是它本身,因为严格来说,int的最小值的相反数已经超出了int的范围。
有了求相反数之后,我们就只用考虑加法运算了。加法运算实现如下:
public static int add(int a, int b) {
int sum = 0;
int carry = 0;
for (int i = 0; i < 32; i++) {
int flag = 1 << i;
int currentIndex = ((a ^ b)) & flag;
int currentCarry = ((a & b) & flag) << 1;
if (carry == 0) {
sum = sum | currentIndex;
carry = currentCarry;
} else {
if (currentIndex == 0) {
sum = sum | flag;
carry = currentCarry;
} else {
carry = 1;
}
}
// System.out.println("currentIndex "+currentIndex+" currentCarry "+currentCarry+" carry "+carry+" sum "+sum);
}
return sum;
}
System.out.println(add(12, 342));
System.out.println(add(-12, 342));
System.out.println(add(-12, -342));
System.out.println(add(Integer.MAX_VALUE, 1));
System.out.println(add(Integer.MIN_VALUE, -1));
System.out.println(add(Integer.MIN_VALUE, 1));
354
330
-354
-2147483648
2147483647
-2147483647
因为补码的性质,我们可以将int当作无符号的数来做相加,结果是正确的。代码中我们从低位开始相加,并判断有无进位,一直计算到最高位。
其中int的最大值加1之后,超过了int的上限,得到的数是和那个实际值关于232同模的值,也就是int的最小值-231。
然后是乘法,乘法我们依旧可以把int当作无符号的数来计算,计算乘法的方法是,对其中一个数进行循环,然后当遇到1的时候,将另一个数王左移,然后就所有左移的结果加起来就是相乘的结果。由于我们已经实现了加法,所以这儿可以直接使用:
public static int multiply(int a, int b) {
int sum = 0;
for (int i = 0; i < 32; i++) {
int flag = 1 << i;
if ((a & flag) == flag) {
sum += b << i;
}
// System.out.println("sum " + sum);
}
return sum;
}
System.out.println(multiply(11, 12));
System.out.println(multiply(-7, -26));
System.out.println(multiply(-5, 9));
System.out.println(multiply(0x10000, 0x10000));
132
182
-45
0
其中0x10000的平方是232次方,它超过了int的范围,,所以结果是int范围内,和232关于232同模的0。
最后关于除法,由于我们已经能够计算相反数,所以我们可以只考虑正数之间的除法,计算除法的方法是,先算出两个数的最高位,然后对被除数王左移到与除数相同的位数,比较大小,如果除数大则在上上加上相应的数,一直循环到被除数不往右移的情况,然后得到最终的商,具体代码如下:
public static int divide(int a, int b) {
if (b > a) {
return 0;
}
//获取a的最高位
int maxA = 0;
for (int i = 0; i < 31; i++) {
int flag = 1 << 31 - i;
if ((a & flag) == flag) {
maxA = 31 - i;
break;
}
}
//获取b的最高位
int maxB = 0;
for (int i = 0; i < 31; i++) {
int flag = 1 << 31 - i;
if ((b & flag) == flag) {
maxB = 31 - i;
break;
}
}
// System.out.println("maxA " + maxA + " maxB " + maxB);
int index = maxA - maxB;
int sum = 0;
for (int i = 0; i <= index; i++) {
if (a > b << (index - i)) {
sum += 1 << (index - i);
a -= b << (index - i);
// System.out.println("sum" + sum);
}
}
return sum;
}
System.out.println(divide(100, 3));
System.out.println(divide(32424, 200));
System.out.println(divide(239, 13));
33
162
18
以上代码使用了大小比较,而大小比较可以通过把两个数相减判断结果的正负方式很简单的实现,所以不在赘述。
总结
综上所述,我们讨论了反码补码以及Java数值在计算机的存储,以及Java位运算的操作,并结合知识这些实现了颜色的获取以及位运算实现加减乘除,相信读者对Java的数值以及位运算符有了更深的认识,以后遇到相关问题的时候也能够很好的解决。