原码、反码、补码
在计算机内,有符号数有3种表示方法:原码、反码和补码,机器数的最高位为符号位,符号位为0表示正数,符号位为1表示负数。
原码=符号位+真值的绝对值。如:3的原码(这里假设机器数的字长为8)是00000011,-3的原码是10000011。
对于正数,反码=原码;对于负数,反码=原码保留符号位+绝对值部分按位取反。如:-3的原码是10000011,反码是11111100。
对于正数,补码=原码;对于负数,补码=反码保留符号位+(绝对值部分+1)。如:-3的原码是10000011,补码是11111101。
总结:对于正数,原码=反码=补码;对于负数,反码=原码保留符号位+绝对值部分按位取反,补码=反码保留符号位+(绝对值部分+1),可见负数的原码反码补码的符号位都是1;对于0,+0和-0有两种不同的原码表示形式,一般统一将-0变为+0,即将0归入正数,所以0的原码=反码=补码。
问:原码的概念很好理解,为什么还要加入反码和补码的概念?
答:因为计算机中底层只能计算加法,所以需要将减法转换成加法来处理。如:3-3=3+(-3),原码不能解决这个问题,补码可以,反码可以看作是为了引出补码的概念。补码可以带符号位参与运算,计算机的数值型数据都是补码的形式存储的。如:3-3=3+(-3),3和-3用补码表示(00000011和11111101)并相加得到的结果是00000000,即0;3-5=3+(-5),-5的原码是10000101,反码是11111010,补码是11111011,3和-5用补码表示(00000011和11111011)并相加得到的结果是11111110,这是补码,符号位为1说明是个负数,将这个补码转换成原码(先得到反码:符号位不变,绝对值部分-1,得11111101,再得到原码:符号位不变,绝对值部分按位取反,得10000010,即-2),根据原码得到结果-2。
代码演示
Java的位运算符有:&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移)、>>(右移)、>>>(无符号右移,注意并没有无符号左移的位运算符)
- 计算机的位只有1、0两个值,计算数值型数据的时候,使用位运算符比使用算术运算符要快。
- 注:位操作符的结果都是int类型。
- a&b:对比a、b的每个位,只有两者均为1时,结果为1,其他时候结果为0。可理解为,比较每个位,每个位取小那个值。
- a|b:对比a、b的每个位,只要两者有一个为1,结果就为1,只有两者全为0时结果为0。可理解为,比较每个位,每个位取大那个值。
- a^b:对比a、b的每个位,两者不同时结果为1,相同时结果为0。异或有翻转的作用。
- ~a:对a的每个位进行取反操作。
- a<<n:左移n位,溢出舍去,少了补n个0。在一些情况下,结果为
a
∗
2
n
a*2^n
a∗2n。
- 注:移位要先求模(包括左移右移无符号右移),再移。a移n位,实际上移动的位数是n%(32或64,a为int型就是32,long型就是64)。
- a>>n:右移n位,溢出舍去,用符号位填充高位。结果为
a
/
2
n
a/2^n
a/2n。
- 注:结果向下取整,10/4=2.5,向下取整等于2;-10/4=-2.5,向下取整等于-3。
- a>>>n:右移n位,溢出舍去,用0填充高位。在一些情况下,结果为 a / 2 n a/2^n a/2n。
public class TestBitOperations {
public static void main(String[] args) {
int a = 0b10; // 2,正数,原码=反码=补码=00000000 00000000 00000000 00000010
int b = 010; // 8,正数,原码=反码=补码=00000000 00000000 00000000 00001000
int c = 10; // 10,正数,原码=反码=补码=00000000 00000000 00000000 00001010
int d = 0x10; // 16,正数,原码=反码=补码=00000000 00000000 00000000 00010000
int negativeA = -a;// 负数,原码=10000000 00000000 00000000 00000010,反码=11111111 11111111 11111111 11111101,补码=11111111 11111111 11111111 11111110
int negativeB = -b;// 负数,原码=10000000 00000000 00000000 00001000,反码=11111111 11111111 11111111 11110111,补码=11111111 11111111 11111111 11111000
int negativeC = -c;// 负数,原码=10000000 00000000 00000000 00001010,反码=11111111 11111111 11111111 11110101,补码=11111111 11111111 11111111 11110110
int negativeD = -d;// 负数,原码=10000000 00000000 00000000 00010000,反码=11111111 11111111 11111111 11101111,补码=11111111 11111111 11111111 11110000
int[] numbers = { a, b, c, d, negativeA, negativeB, negativeC, negativeD};
for (int element : numbers) {
// Integer.toBinaryString(int i)只有在i为负数时,会返回补码
System.out.printf("%d 在计算机中存储的是 %s%n", element, formatBinaryString(intToBinaryString(element)));
}
System.out.println("--------------------------------------------------------------------------------");
showBitOperations(b, c, 2);
System.out.println("--------------------------------------------------------------------------------");
showBitOperations(c, d, 2);
System.out.println("--------------------------------------------------------------------------------");
showBitOperations(negativeB, negativeC, 2);
System.out.println("--------------------------------------------------------------------------------");
// 移位要先根据移的位数求模(int类型的数据模是32,long类型的数据模是64),再移。
System.out.printf("%d(%s) 左移(<<) %d位得到%d(%s)%n", a, formatBinaryString(intToBinaryString(a)), 34, c, formatBinaryString(intToBinaryString(c)));
System.out.println("--------------------------------------------------------------------------------");
// 判断奇偶
for (int i = 0; i < 10; i++) {
String str = (i & 1) == 1 ? "是奇数" : "是偶数";
System.out.println(i + str);
}
System.out.println("--------------------------------------------------------------------------------");
}
/**
* 将int值转换为二进制字符串表示。
*
* @param n 要转换的int值
* @return 对应的二进制字符串
*/
private static String intToBinaryString(int n) {
StringBuilder binary = new StringBuilder(32);
for (int i = 31; i >= 0; i--) {
// 通过位与运算和位移操作获取每一位的值,并追加到字符串中
binary.append((n >> i & 1));
}
return binary.toString();
}
/**
* 每8位加个空格,方便观察
* @param binaryString 输入的二进制字符串
* @return 格式化后的字符串
*/
private static String formatBinaryString(String binaryString) {
if (binaryString == null) {
return null; // 或者抛出异常,根据实际需求处理null输入
}
StringBuilder sb = new StringBuilder();
int length = binaryString.length();
for (int i = 0; i < length; i++) {
sb.append(binaryString.charAt(i)); // 添加当前字符
if ((i + 1) % 8 == 0) { // 检查是否达到每隔8字符的条件
sb.append(" "); // 添加空格
}
}
return sb.toString(); // 返回格式化后的字符串
}
/**
* 演示Java的位运算符(&、|、^、~、<<、>>、>>>)
* @param a
* @param b
* @param n 位移几位
*/
private static void showBitOperations(int a, int b, int n) {
// 按位与
int aAndB = a & b;
System.out.printf("%d(%s) 按位与(&) %d(%s)得到%d(%s)%n", a, formatBinaryString(intToBinaryString(a)), b, formatBinaryString(intToBinaryString(b)), aAndB, formatBinaryString(intToBinaryString(aAndB)));
// 按位或
int aOrB = a | b;
System.out.printf("%d(%s) 按位或(|) %d(%s)得到%d(%s)%n", a, formatBinaryString(intToBinaryString(a)), b, formatBinaryString(intToBinaryString(b)), aOrB, formatBinaryString(intToBinaryString(aOrB)));
// 按位异或(不同为1,同为0)
int aXorB = a ^ b;
System.out.printf("%d(%s) 按位异或(^) %d(%s)得到%d(%s)%n", a, formatBinaryString(intToBinaryString(a)), b, formatBinaryString(intToBinaryString(b)), aXorB, formatBinaryString(intToBinaryString(aXorB)));
// 按位取反
int notA = ~a;
int notB = ~b;
System.out.printf("%d(%s) 按位取反(~)后得到 %d(%s)%n", a, formatBinaryString(intToBinaryString(a)), notA, formatBinaryString(intToBinaryString(notA)));
System.out.printf("%d(%s) 按位取反(~)后得到 %d(%s)%n", b, formatBinaryString(intToBinaryString(b)), notB, formatBinaryString(intToBinaryString(notB)));
// 左移
int leftShiftA = a << n;
int leftShiftB = b << n;
System.out.printf("%d(%s) 左移(<<) %d位得到%d(%s)%n", a, formatBinaryString(intToBinaryString(a)), n, leftShiftA, formatBinaryString(intToBinaryString(leftShiftA)));
System.out.printf("%d(%s) 左移(<<) %d位得到%d(%s)%n", b, formatBinaryString(intToBinaryString(b)), n, leftShiftB, formatBinaryString(intToBinaryString(leftShiftB)));
// 右移
int rightShiftA = a >> n;
int rightShiftB = b >> n;
System.out.printf("%d(%s) 右移(>>) %d位得到%d(%s)%n", a, formatBinaryString(intToBinaryString(a)), n, rightShiftA, formatBinaryString(intToBinaryString(rightShiftA)));
System.out.printf("%d(%s) 右移(>>) %d位得到%d(%s)%n", b, formatBinaryString(intToBinaryString(b)), n, rightShiftB, formatBinaryString(intToBinaryString(rightShiftB)));
// 无符号右移
int unsignedRightShiftA = a >>> n;
int unsignedRightShiftB = b >>> n;
System.out.printf("%d(%s) 无符号右移(>>>) %d位得到%d(%s)%n", a, formatBinaryString(intToBinaryString(a)), n, unsignedRightShiftA, formatBinaryString(intToBinaryString(unsignedRightShiftA)));
System.out.printf("%d(%s) 无符号右移(>>>) %d位得到%d(%s)%n", b, formatBinaryString(intToBinaryString(b)), n, unsignedRightShiftB, formatBinaryString(intToBinaryString(unsignedRightShiftB)));
}
}
运行结果:
应用
-
判断某数的奇偶:
(n & 1) == 1
(true为奇、false为偶) -
判断两个数是否异号:
(a^b)<0
(true为异号、false为同号) -
速算某数*2的n次方:
某数<<n
- 注:移位要先求模(包括左移右移无符号右移),再移。某数移n位,实际上移动的位数是n%(32或64,某数为int型就是32,long型就是64)。
- 注:左移可能导致符号位改变,此时结果就不对了。
- 注:左移可能导致结果超过某数数据类型的上限(int型是正负21亿多),此时结果就不对了。
-
速算某数/2的n次方:
某数>>n
- 注:移位要先求模(包括左移右移无符号右移),再移。某数移n位,实际上移动的位数是n%(32或64,某数为int型就是32,long型就是64)。
- 注:结果向下取整,10/4=2.5,向下取整等于2;-10/4=-2.5,向下取整等于-3。
-
某数+1:
-~某数
- 证明:因为n和~n的每个位相加都是1(包括符号位),所以n+(~n)=-1,所以n+1=-~n。
-
某数-1:
~-某数
- 证明:由n+(~n)=-1,得~n=-n-1,即(-n)-1=~-(-n),换元法可得n-1=~-n。
-
不用临时变量交换两个数:
int a = 8; int b = 10; a = a^b; b = b^a; a = a^b;
- 证明:需要用到^的性质。
① a ^ a =0 (任何数异或本身结果为0)
② a ^ b =b ^ a (交换律)
③ a ^ b ^ c = a ^ (b ^ c) = (a ^ b) ^ c (结合律)
④ 0 ^ a = a (异或0具有保持的特点)
⑤ a ^ b ^ a = b (根据①②④可得)
a=a原^b原,b=b原^a=b原^(a原^b原)=a原,至此,a存放的数据为a原^b原,b存放的数据为a原,只要再执行a^a原就能实现a里面存放b原的数据了,但a原的数据已经不在a里面了,而是在b里面了,所以a=a^b=(a原^b原)^a原=b原,完成交换。
- 证明:需要用到^的性质。
-
求某数的绝对值:
|n| = (n^(n>>31))-(n>>31)
(这里的n是32位的数据)- 证明:(TODO)
-
字母转小写:
字母|" "
- 证明:空格的ASCII码是32(即00100000),字母的ASCII码的范围是A~Z(65~90,即010000001~01011010)、a~z(97~122,即01100001~01111010),每个小写字母比对应的大写字母的ASCII码大32(即00100000),刚好为空格的ASCII码。
字母|" "
表示比较字母和空格的每一个位,取大那个值,这样可起到将字母的第三个位恒定设为1,字母的其他位数据不变,结果就相当于将字母转小写(如果字母原来是小写,就不变)。
- 证明:空格的ASCII码是32(即00100000),字母的ASCII码的范围是A~Z(65~90,即010000001~01011010)、a~z(97~122,即01100001~01111010),每个小写字母比对应的大写字母的ASCII码大32(即00100000),刚好为空格的ASCII码。
-
字母转大写:
字母&"_"
- 证明:下划线的ASCII码是95(即01011111),
字母&"_"
表示比较字母和_的每一个位,取最小那个值,这样可起到将字母的第三个位恒定设为0,字母的其他位数据不变,结果就相当于将字母转大写(如果字母原来是大写,就不变)。
- 证明:下划线的ASCII码是95(即01011111),
-
大小写互换:
字母^" "
- 证明:
字母^" "
表示字母的第三个位10互换(1^1=0,0^1=1),字母的其他位数据不变,结果就相当于将字母切换大小写。
- 证明:
参考
- 《Java核心技术 卷1 基础知识 原书第11版》 机械工业出版社 Cay S.Horstmann著 林琪、苏钰涵等译
- Java中的位运算 by 柯柯不会Java
- 一篇搞定位运算——java位运算详解 by 青癯