【方向盘】Java二进制和位运算,这一万字准能喂饱你(下)

简介: 【方向盘】Java二进制和位运算,这一万字准能喂饱你(下)

位运算使用场景示例


位运算除了高效的特点,还有一个特点在应用场景下不容忽视:计算的可逆性。通过这个特点我们可以用来达到隐蔽数据的效果,并且还保证了效率。

在JDK的原码中。有很多初始值都是通过位运算计算的。最典型的如HashMap:


HashMap:
  static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
  static final int MAXIMUM_CAPACITY = 1 << 30;


位运算有很多优良特性,能够在线性增长的数据中起到作用。且对于一些运算,位运算是最直接、最简便的方法。下面我安排一些具体示例(一般都是面试题),感受一把。


判断两个数字符号是否相同


同为正数or同为负数都表示相同,否则为不同。像这种小小case用十进制加上>/<比较符当然可以做,但用位运算符处理来得更加直接(效率最高):


@Test
public void test4() {
    int i = 100;
    int j = -2;
    System.out.println(((i >> 31) ^ (j >> 31)) == 0);
    j = 10;
    System.out.println(((i >> 31) ^ (j >> 31)) == 0);
}


运行程序,输出:

false
true



int类型共32bit,右移31位那么就只剩下1个符号位了(因为是带符号右移动,所以正数剩0负数剩1),再对两个符号位做^异或操作结果为0就表明二者一致。


复习一下^异或操作规则:相同为0,不同为1。


判断一个数的奇偶性


在十进制数中可以通过和2取余来做,对于位运算有一个更为高效的方式:


@Test
public void test5() {
    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) {
    return (n & 1) == 0;
}


为何&1能判断基偶性?因为在二进制下偶数的末位肯定是0,奇数的最低位肯定是1。

而二进制的1它的前31位均为0,所以在和其它数字的前31位与运算后肯定所有位数都是0(无论是1&0还是0&0结果都是0),那么唯一区别就是看最低位和1进行与运算的结果喽:结果为1表示奇数,反则结果为0就表示偶数。


交换两个数的值(不借助第三方变量)


这是一个很古老的面试题了,交换A和B的值。本题如果没有括号里那几个字,是一道大家都会的题目,可以这么来解:


@Test
public void test6() {
    int a = 3, b = 5;
    System.out.println(a + "-------" + b);
    a = a + b;
    b = a - b;
    a = a - b;
    System.out.println(a + "-------" + b);
}


运行程序,输出(成功交换):

3-------5
5-------3


使用这种方式最大的好处是:容易理解。最大的坏处是:a+b,可能会超出int型的最大范围,造成精度丢失导致错误,造成非常隐蔽的bug。所以若你这样运用在生产环境的话,是有比较大的安全隐患的。


小贴士:如果你们评估数字绝无可能超过最大值,这种做法尚可。当然如果你是字符串类型,请当我没说


因为这种方式既引入了第三方变量,又存在重大安全隐患。所以本文介绍一种安全的替代方式,借助位运算的可逆性来完成操作:

@Test
public void test7() {
    // 这里使用最大值演示,以证明这样方式是不会溢出的
    int a = Integer.MAX_VALUE, b = Integer.MAX_VALUE - 10;
    System.out.println(a + "-------" + b);
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    System.out.println(a + "-------" + b);
}


运行程序,输出(成功完成交换):

2147483647-------2147483637
2147483637-------2147483647



由于全文都没有对a/b做加法运算,因此不能出现溢出现象,所以是安全的。这种做法的核心原理依据是:位运算的可逆性,使用异或来达成目的。


位运算用在数据库字段上(重要)


这个使用case是极具实际应用意义的,因为在生产上我以用过多次,感觉不是一般的好。


业务系统中数据库设计的尴尬现象:通常我们的数据表中可能会包含各种状态属性, 例如 blog表中,我们需要有字段表示其是否公开,是否有设置密码,是否被管理员封锁,是否被置顶等等。 也会遇到在后期运维中,策划要求增加新的功能而造成你需要增加新的字段,这样会造成后期的维护困难,字段过多,索引增大的情况, 这时使用位运算就可以巧妙的解决。


举个例子:我们在网站上进行认证授权的时候,一般支持多种授权方式,比如:


  • 个人认证 0001 -> 1
  • 邮箱认证 0010 -> 2
  • 微信认证 0100 -> 4
  • 超管认证 1000 -> 8


这样我们就可以使用1111这四位来表达各自位置的认证与否。要查询通过微信认证的条件语句如下:


select * from xxx where status = status & 4;


要查询既通过了个人认证,又通过了微信认证的:

select * from xxx where status = status & 5;


当然你也可能有排序需求,形如这样:

select * from xxx order by status & 1 desc


这种case和每个人都熟悉的Linux权限控制一样,它就是使用位运算来控制的:权限分为 r 读, w 写, x 执行,其中它们的权值分别为4,2,1,你可以随意组合授权。比如 chomd 7,即7=4+2+1表明这个用户具有rwx权限,


注意事项


  1. 需要你的DB存储支持位运算,比如MySql是支持的
  2. 请确保你的字段类型不是char字符类型,而应该是数字类型
  3. 这种方式它会导致索引失效,但是一般情况下状态值是不需要索引的
  4. 具体业务具体分析,别一味地为了show而用,若用错了容易遭对有喷的


流水号生成器(订单号生成器)


生成订单流水号,当然这其实这并不是一个很难的功能,最直接的方式就是日期+主机Id+随机字符串来拼接一个流水号,甚至看到非常多的地方直接使用UUID,当然这是非常不推荐的。


UUID是字符串,太长,无序,不能承载有效的信息从而不能给定位问题提供有效帮助,因此一般属于备选方案


今天学了位运算,有个我认为比较优雅方式来实现。什么叫优雅:可以参考淘宝、京东的订单号,看似有规律,实则没规律:


  • 不想把相关信息直接暴露出去。
  • 通过流水号可以快速得到相关业务信息,快速定位问题(这点非常重要,这是UUID不建议使用的最重要原因)。
  • 使用AtomicInteger可提高并发量,降低了冲突(这是不使用UUID另一重要原因,因为数字的效率比字符串高)


实现原理简介


此流水号构成:日期+Long类型的值 组成的一个一长串数字,形如2020010419492195304210432。很显然前面是日期数据,后面的一长串就蕴含了不少的含义:当前秒数、商家ID(也可以是你其余的业务数据)、机器ID、一串随机码等等。


各部分介绍:


  1. 第一部分为当前时间的毫秒值。最大999,所以占10位
  2. 第二部分为:serviceType表示业务类型。比如订单号、操作流水号、消费流水号等等。最大值定为30,足够用了吧。占5位
  3. 第三部分为:shortParam,表示用户自定义的短参数。可以放置比如订单类型、操作类型等等类别参数。最大值定为30,肯定也是足够用了的。占5位
  4. 第四部分为:longParam,同上。用户一般可放置id参数,如用户id、商家id等等,最大支持9.9999亿。绝大多数足够用了,占30位
  5. 第五部分:剩余的位数交给随机数,随机生成一个数,占满剩余位数。一般至少有15位剩余(此部分位数是浮动的),所以能支持2的15次方的并发,也是足够用了的
  6. 最后,在上面的long值前面加上日期时间(年月日时分秒)


这是A哥编写的一个基于位运算实现的流水号生成工具,已用于生产环境。考虑到源码较长(一个文件,共200行左右,无任何其它依赖)就不贴了,若有需要,请到公众号后台回复流水号生成器免费获取。


✍总结


位运算在工程的角度里缺点还是蛮多的,在实际工作中,如果只是为了数字的计算,是不建议使用位运算符的,只有一些比较特殊的场景,使用位运算去做会给你柳暗花明的感觉,如:


  • N多状态的控制,需要兼具扩展性。比如数据库是否状态的字段设计
  • 对效率有极致要求。比如JDK
  • 场景非常适合。比如Jackson的Feature特针值


切忌为了炫(zhuang)技(bi)而使用,炫技一时爽,掉坑火葬场;小伙还年轻,还望你谨慎。代码在大多情况下,人能容易读懂比机器能读懂来得更重要。

相关文章
|
2月前
|
Java
Java中整数(负数)的二进制表示
Java中整数(负数)的二进制表示
|
4月前
|
Java
Java打印二进制
Java打印二进制
45 0
|
2月前
|
Java
Java中将一个数转化为二进制
Java中将一个数转化为二进制
25 0
|
4月前
|
Java
Java 运算符详解(取模、位运算、运算符的执行顺序)
Java 运算符详解(取模、位运算、运算符的执行顺序)
53 0
|
11天前
|
存储 Java 数据安全/隐私保护
【Java探索之旅】运算符解密 位运算,移位运算
【Java探索之旅】运算符解密 位运算,移位运算
20 0
|
2月前
|
算法 Java 数据处理
Java:将一个数转化为二进制
Java:将一个数转化为二进制
|
2月前
|
机器学习/深度学习 Java 程序员
Java基础之二进制,八进制,十进制,十六进制相互转换
Java基础之二进制,八进制,十进制,十六进制相互转换
|
4月前
|
算法 C++ Java
Java每日一练(20230423) 数组元素统计、杨辉三角II、二进制求和
Java每日一练(20230423) 数组元素统计、杨辉三角II、二进制求和
30 0
Java每日一练(20230423) 数组元素统计、杨辉三角II、二进制求和
|
9月前
|
Java
Java 中位运算,原码,反码,补码的详解
Java 中位运算,原码,反码,补码的详解
75 0
|
9月前
|
人工智能 Java BI
【Java基础】位运算实现加减乘除
&:按位与,对应位都为 1 时结果为 1,否则为 0。 |:按位或,对应位有一个为 1 时结果为 1,否则为 0。 ^:按位异或,对应位不同时结果为 1,否则为 0。 ~:按位取反,将每一位取反。 <<:左移,将所有位向左移动指定的位数,右边补零。
68 0