~:按位非
操作规则:全部的0置为1,1置为0。
public static void main(String[] args) { // 2 -> 10(其实是00000000000000000000000000000010 共32位) // 非后结果: 11111111111111111111111111111101 共32位 System.out.println(Integer.toBinaryString(~2)); }
可以看到取非的结果像是“面目全非”的赶脚,因此使用时需要谨慎。
操作规则:操作数不同时(1遇上0,0遇上1)对应的输出结果才为1,否则为0。(相同为0,不同为1)
public static void main(String[] args) { // 2 -> 10 // 3 -> 11 // 异或后结果:01(二进制数) System.out.println(Integer.toBinaryString(2 ^ 3)); }
<<:按位左移
操作规则:把一个数的全部位数都向左移动若干位。
public static void main(String[] args) { // 2 -> 10 // 左移3位结果:10000(二进制数) System.out.println(Integer.toBinaryString(2 << 3)); }
左移用得非常多,也非常好理解。x左移多少位,效果同十进制里直接乘以2的多少次方就行了,但是需要注意值溢出的情况~
操作规则:把一个数的全部位数都向右移动若干位。
public static void main(String[] args) { // 2 -> 10 // 右移3位结果:0(二进制数) // 位数不够全被移没了,所以最终打印0 System.out.println(Integer.toBinaryString(2 >> 3)); // 100 -> 1100100 // 右移3位结果:1100 System.out.println(Integer.toBinaryString(100 >> 3)); }
右移用得也很多,操作其实就是吧右边的N位直接砍掉即可
>>>:无符号右移(注意:没有无符号左移)
注意:并没有<<<这个符号的哟~~~
正数做>>>运算的时候和>>是一样的。区别在于负数运算
复合运算
这里指的复合运算指的就是和=号一起来使用,类似于+= -=等。本来这属于常识不用单独解释,但因有好几个小伙伴问过了,所以在此处顺带的介绍下吧:
public static void main(String[] args) { // 2 -> 10 // 3 -> 11 // 与后结果:10(二进制数) int i = 2; i &= 3; // 此效果同 i = i & 3 System.out.println(Integer.toBinaryString(i)); //打印:10 }
位运算的使用场景
位运算不仅有高效的特点,还有一个非常非常的大特点:计算的可逆性。通过这个特点我们可以用来达到隐蔽数据的效果(后面有示例),并且还保证了效率。
在JDK的原码中。有很多初始值都是通过位运算计算的。位运算有很多优良特性,能够在线性增长的数据中起到作用。且对于一些运算,位运算是最直接、最简便的方法。
判断一个数的奇偶性
在十进制数中可以通过和2取余来可以达到效果,对于位运算有一个更为高效的方式:
public static void main(String[] args) { System.out.println(isEvenNum(1)); //false System.out.println(isEvenNum(2)); //true System.out.println(isEvenNum(3)); //false System.out.println(isEvenNum(4)); //true System.out.println(isEvenNum(5)); //false } private static boolean isEvenNum(int n) { // 1 -> 1(二进制表示。所以它的前31位都是0哦~~~) return (n & 1) != 1; }
为何&1能判断基偶性?因为在二进制下偶数的末位肯定是0,so奇数的最低位肯定是1。
而二进制的1它的前31位均为0,所以在和其它数字的前31位与运算后肯定所有位数都是0(无论是1&0还是0&0结果都是0),那么唯一区别就是看最低位和1进行与运算的结果喽:结果为1表示奇数,反则结果为0表示偶数
不借助第三方变量方式交换两个数的值
这是个经典面试题,题目本来很简单,但是加上了不借助第三方变量这个条件后就会难倒一大片了。其实它会有两种方案,这里我都展示出来:
方式一:传统方式
public static void main(String[] args) { int a = 3, b = 5; System.out.println(a + "-------" + b); a = a + b; b = a - b; a = a - b; System.out.println(a + "-------" + b); }
使用这种方式的好处是容易理解,坏处是:a+b,可能会超出int型的最大范围,造成精度丢失导致错误,所以生产环境强烈建议采用下面的方式二。
方式二:位运算方式
public static void main(String[] args) { // 这里使用最大值演示,以证明这样方式是不会溢出的 int a = Integer.MAX_VALUE, b = Integer.MAX_VALUE - 10; System.out.println(a + "-------" + b); // 2147483647-------2147483637 a = a ^ b; b = a ^ b; a = a ^ b; System.out.println(a + "-------" + b); // 2147483637-------2147483647 }
它的根本原理就是利用了位运算的可逆性,使用异或运算来操作。
移位运算用在数据库字段上
业务系统中数据库设计的尴尬现象:通常 我们的数据表中 可能会包含各种状态属性, 例如 blog表中,我们需要有字段表示其是否公开,是否有设置密码,是否被管理员封锁,是否被置顶等等。 也会遇到在后期运维中,策划要求增加新的功能而造成你需要增加新的字段,这样会造成后期的维护困难,数据库增大,索引增大的情况, 这时使用位运算就可以巧妙的解决。
说明:1、MySql是支持这些位运算符的;2、这种方式不一定适合所有场景,因为它会导致索引失效(不过状态值一般也不需要索引),所以具体问题需具体分析
其实移位运算玩法比较像Linux里的权限控制:权限分为 r 读, w 写, x 执行,其中 它们的权值分别为4,2,1, 所以 如果用户要想拥有这三个权限 就必须 chomd 7 , 即 7=4+2+1 表明 这个用户具有rwx权限,如果只想这个用户具有r,x权限 那么就 chomd 5即可。
流水号生成器(订单号生成器)
生成订单流水号,当然这其实这并不是一个很难的功能,最直接的方式就是日期+主机Id+随机字符串来拼接一个流水号,但是今天有个我认为比较优雅方式来实现。什么叫优雅:可以参考淘宝、京东的订单号,看似有规律,其实没规律:
- 不想把相关信息直接暴露出去。
- 通过流水号可以快速得到相关业务信息,快速定位问题(这点非常重要)。
- 使用AtomicInteger可提高并发量,降低了冲突