位运算-详细总结
简介
位运算:直接对二进制数进行操作的运算,因为是直接对二进制数进行计算,所以相较于普通的算数运算,位运算的效率很高。
位运算通常用于计算整数,对于小数的运算则不太常见。因为整数在内存中使用补码来存储,它是线性的、固定的,小数在内存则使用浮点数来存储,浮点数包含符号位、指数位、尾数位,使用位运算来计算浮点数会很复杂并且难以理解。
内部原理
整数在内存中的存储方式
在使用位运算计算整数之前,先了解一下整数在内存中的存储方式,因为一会儿会使用位运算来操作整数。
整数在内存中使用补码来存储,补码是由原码、反码转换而来
原码、反码、补码:
- 原码:最高位为符号位,0代表正数,1代表负数,非符号位为该数字绝对值的二进制表示。原码是一种表示有符号整数的编码方式
- 反码:正数的反码和原码一致,负数的反码是对原码除符号位外按位取反
- 补码:正数的补码和原码一致,负数的补码是反码加1
机器数:一个数的二进制表示形式,机器数是带符号的,0表示正数,1表示负数,机器数对应的真正的数值称为机器数的真值。原码是机器数的一种具体表示形式,用于表示有符号整数。
计算机为什么要使用补码来存储整数:为了解决整数的加减运算问题。
- 如果计算机内部采用原码来表示数,那么在进行加法和减法运算的时候,需要转化为两个绝对值的加法和减法运算,计算机既要实现加法器,又要实现减法器,代价比较大。
- 解决这个问题的办法就是化减为加,原理是使用计量系统中模的概念
- 模:指一个计量系统的计数范围,是计量器产生‘溢出’的量,它的值在计量器上表示不出来,计量器只能表示出模的余数,任何有模的计量器,均可化减法为加法运算。例如时钟,它的计量范围是0-11,模是12。在以12为模的系统中,加8和减4的效果是一样的,因此凡是减4的运算,都可以用加8来代替,对模而言, 8和4互为补数,11和1、10和2也都有这个特性。对于计算机,其概念和方法完全一样。计算机中一字节8比特,计数范围是0-255,模是256,等于2的8次方,减2就相当于加上2的补数254,然后再将进位舍弃掉。根据这个原理,科学家发明了补码,负数的补码等于模减去该数的绝对值,使用补码在计算机中进行运算,可以化减法为加法。
案例1:一个数的原码、反码、补码的转换
数字 5:正数的原码、反码、补码都一样原码:0000 0101 = 5反码:0000 0101 = 5补码:0000 0101 = 5数字 -5:原码:1000 0101 = -5反码:1111 1010 = -5 // 对原码除符号位外按位取反补码:1111 1011 = -5 // 反码加 1
案例2:使用补码进行减法计算:例如,7 - 5,相当于 7 + (-5)。
0000 0111 = 7的补码+ 1111 1011 = -5的补码----------------------------1 0000 0010 去掉溢出的模数256,结果等于2
基本使用
位运算符:
- 按位与:
&
,遇0则0 - 按位或:
|
,遇1则1 - 按位异或:
^
,相同为0,不同为1 - 按位取反:
~
,一元运算符,二进制数0变1、1变0。取反操作连符号位也要进行转换 - 左位移:
<<
,二进制数整体向左位移多少位,右边补0。左位移时,第一个比特位向左位移29位,现在在第30位 - 右位移:
>>
,二进制数整体向右位移多少位,左边补上符号位,整数补0,负数补1 - 无符号右位移:
>>>
,右移之后,无论该数为正还是为负,右移之后左边都是补上0。只有java、scala等才有无符号右位移运算。
案例1:位运算的基本使用
public static void main(String[] args) {int a = 10;System.out.println("a的二进制形式 = " + Integer.toBinaryString(a)); // 1010,这里省略了前面的0System.out.println("a & 1 = " + (a & 1)); // a和1按位与,遇0则0,结果为0System.out.println("a | 1 = " + (a | 1)); // a和1按位或,遇1则1,结果为11System.out.println("a ^ 1 = " + (a ^ 1)); // a和1按位异或,相同为0,不同为1,结果为11System.out.println("a >> 1 = " + (a >> 1)); // a右位移1位,相当于除以2,结果为5System.out.println("a << 1 = " + (a << 1)); // a左位移1位,相当于乘以2,结果为20System.out.println("a >>> 1 = " + (a >>> 1)); // a无符号右位移1位,相当于除以2,结果为5
}
案例2:位运算中的取反操作,连符号位也要进行转换
@Test
public void test1() {int x = 1;int y = ~x;System.out.println("y = " + y); // -2System.out.println("x的二进制 = " + Integer.toBinaryString(x)); // 0000 0000 0000 0000 0000 0000 0000 0001System.out.println("y的二进制 = " + Integer.toBinaryString(y)); // 1111 1111 1111 1111 1111 1111 1111 1110
}
使用场景
位运算实现乘除法
例如,向左位移一位,相当于乘以2,向右位移一位,相当于除以2。
问题:用最有效率的方法计算2乘以8?2 << 3,左移3位相当于乘以2的3次方,2的3次方就是8
二进制的取位操作
按位与通常用于二进制的取位操作,例如,一个数和1进行按位与的结果就是取二进制的最末位,这可以用来判断一个整数的奇偶。
二进制特定位上的无条件赋值
按位或,通常用于二进制特定位上的无条件赋值,例如,一个数和1进行按位或的结果,就是二进制的最末位变成1
按位异或可以用于加密
一个数被同一个数异或两次,结果等于它本身,可以用于加密数据。
使用位运算,在不使用第三个变量的前提下交换两个变量
案例:
a = a ^ b;
b = a ^ b; // 此时b = a ^ b ^ b,a连续两次异或b,结果是a本身,所以b = a
a = a ^ b // 此时a = a ^ b,b = a,所以,a = a ^ b ^ a,a = b
使用位运算实现正数和负数的互换
正数取反加1,变成它对应的负数,负数取反加1,变成它对应的正数,这是原码、反码、补码的转换逻辑
案例:
public static void main(String[] args) {int a = 5;int b = ~a + 1;System.out.println("b = " + b); // -5int c = -5;int d = ~c + 1;System.out.println("d = " + d); // 5
}
实际案例
反射中的位运算: Modifier访问修饰符
Modifier中定义了访问修饰符的枚举值和计算逻辑。
相关源码:
public class Modifier {// 每一个访问修饰符都使用一个数字来表示,这里的数字是16进制的。public static final int PUBLIC = 0x00000001; // 000001public static final int PRIVATE = 0x00000002; // 000010public static final int PROTECTED = 0x00000004; // 000100public static final int STATIC = 0x00000008; // 001000public static final int FINAL = 0x00000010; // 010000public static final int SYNCHRONIZED = 0x00000020; // 100000public static final int VOLATILE = 0x00000040; // 以此类推public static final int TRANSIENT = 0x00000080;public static final int NATIVE = 0x00000100;public static final int INTERFACE = 0x00000200;public static final int ABSTRACT = 0x00000400;public static final int STRICT = 0x00000800;// 访问修饰符的计算,这里计算一个元素是否是publicpublic static boolean isPublic(int mod) {// 按位与,遇0则0,例如,如果字段的修饰符是 public static,那么它的访问修饰符是 1001,// 它和PUBLIC(0001)按位与,结果是0001,不为0,证明字段被public关键字修饰。因为按位与遇0则0,// 只有传入的访问修饰符PUBLIC位上不为0,按位与的结果才不为0return (mod & PUBLIC) != 0; }
}
通过Modifier类,可以判断一个元素有什么访问修饰符。它的原理是,每一个访问修饰符使用一个2的倍数来表示,如果一个元素有多个访问修饰符,那么就是多个访问修饰符执行或运算,组合在一起,如果需要判断元素上有没有指定访问修饰符,就是元素的访问修饰符和修饰符常量进行与运算,结果不为0就表示元素上有该修饰符。这应该算是比较简单的位运算,很有参考价值,在设计状态值枚举的时候,尤其是一个实体可以同时拥有多种状态的情况下很有用,否则使用简单的枚举类就可以了。