Java 二进制和位运算,这一万字准能喂饱你
基础不牢,地动山摇。本文已被 https://www.yourbatman.cn 收录,里面一并有 Spring 技术栈、MyBatis、JVM、中间件等小而美的专栏供以免费学习。关注公众号【BAT 的乌托邦】逐个击破,深入掌握,
✍前言
你好,我是 YourBatman。
本号正在连载 Jackson 深度解析系列,虽然目前还只讲到了其流式 API 层面,但已接触到其多个Feature
特征。更为重要的是我在文章里赞其设计精妙,处理优雅,因此就有小伙伴私信给我问这样的话:
题外话:Jackson 这个话题本就非常小众,看着阅读量我自己都快没信心写下去。但自己说过的话就是欠下的债,熬夜也得把承诺的付费内容给公开完了,毕竟还有那么几个人在白嫖不是😄。
话外音:以后闷头做事,少吹牛逼┭┮﹏┭┮
虽然小众,竟然还有想深入了解一波的小伙伴,确实让我为之振奋了那么三秒。既然如此那就干吧,本文就先行来认识认识 Java 中的位运算。位运算在 Java 中很少被使用,那么为何 Jackson 里爱不释手呢?一切就为两字:性能/*高效*。用计算机能直接看懂的语言跟它打交道,你说快不快,不用多想嘛。
✍正文
提及位运算,对绝大多数 Java 程序员来说,是一种既熟悉又陌生的感觉。熟悉是因为你在学 JavaSE 时肯定学过,并且在看一些开源框架(特别是 JDK 源码)时都能看到它的身影;陌生是因为大概率我们不会去使用它。当然,不能“流行”起来是有原因的:不好理解,不符合人类的思维,阅读性差…...
小贴士:一般来说,程序让人看懂远比被机器看懂来得更重要些
位运算它在low-level
的语言里使用得比较多,但是对于 Java 这种高级语言它就很少被提及了。虽然我们使用得很少但 Java 也是支持的,毕竟很多时候使用位运算才是最佳实践。
位运算在日常开发中使用得较少,但是巧妙的使用位运算可以大量减少运行开销,优化算法。一条语句可能对代码没什么影响,但是在高重复,大数据量的情况下将会节省很多开销。
二进制
在了解什么是位运算之前,十分有必要先科普下二进制的概念。
二进制是计算技术中广泛采用的一种数制。二进制数据是用 0 和 1 两个数码来表示的数。它的基数为 2,进位规则是逢二进一,借位规则是借一当二。因为它只使用 0、1 两个数字符号,非常简单方便,易于用电子方式实现。
小贴士:半导体开代表 1,关代表 0,这也就是 CPU 计算的最底层原理😄
先看一个例子:
二进制理解起来非常非常的简单,比 10 进制简单多了。你可能还会思考二进制怎么和十进制互转呢?毕竟 1110 这个也看不到啊。有或者往深了继续思考:如何转为八进制、十六进制、三十二进制......进制转换并非本文所想讲述的内容,请有兴趣者自行度娘。
二进制与编码
这个虽然和本文内容关联系并不是很大,但顺带捞一捞,毕竟编码问题在开发中还是比较常见的。
计算机能识别的只有 1 和 0,也就是二进制,1 和 0 可以表达出全世界的所有文字和语言符号。那如何表达文字和符号呢?这就涉及到字符编码了。字符编码强行将每一个字符对应一个十进制数字(请注意字符和数字的区别,比如0
字符对应的十进制数字是48
),再将十进制数字转换成计算机理解的二进制,而计算机读到这些 1 和 0 之后就会显示出对应的文字或符号。
一般对英文字符而言,一个字节表示一个字符,但是对汉字而言,由于低位的编码已经被使用(早期计算机并不支持中文,因此为了扩展支持,唯一的办法就是采用更多的字节数)只好向高位扩展
字符集编码的范围
utf-8>gbk>iso-8859-1(latin1)>ascll
。ascll 编码是美国标准信息交换码的英文缩写,包含了常用的字符,如阿拉伯数字,英文字母和一些打印符号共 255 个(一般说成共 128 个字符问题也不大)
UTF-8
:一套以 8 位为一个编码单位的可变长编码,会将一个码位(Unicode)编码为 1 到 4 个字节(英文 1 字节,大部分汉字 3 字节)。
Java 中的二进制
在 Java7 版本以前,Java 是不支持直接书写除十进制以外的其它进制字面量。但这在 Java7 以及以后版本就允许了:
二进制:前置 0b/0B
八进制:前置 0
十进制:默认的,无需前置
十六进制:前置 0x/0X
结果程序,输出:
说明:System.out.println()
会先自动转为 10 进制后再输出的;toBinaryString()
表示转换为二进制进行字符串进行输出。
便捷的进制转换 API
JDK 自1.0
开始便提供了非常便捷的进制转换的 API,这在我们有需要时非常有用。
运行程序,输出:
如何证明 Long 是 64 位的?
我相信每个 Javaer 都知道 Java 中的 Long 类型占 8 个字节(64 位),那如何证明呢?
小贴士:这算是一道经典面试题,至少我提问过多次~
有个最简单的方法:拿到 Long 类型的最大值,用 2 进制表示转换成字符串看看长度就行了,代码如下:
运行程序,输出:
说明:在计算机中,负数以其正值的补码的形式表达。因此,用同样的方法你可以自行证明 Integer 类型是 32 位的(占 4 个字节)。
Java 中的位运算
Java 语言支持的位运算符还是非常多的,列出如下:
&
:按位与|
:按位或~
:按位非^
:按位异或<<
:左位移运算符>>
:右位移运算符>>>
:无符号右移运算符
除~
以 外,其余均为二元运算符,操作的数据只能是整型(长短均可)或者 char 字符型。针对这些运算类型,下面分别给出示例,一目了然。
既然是运算,依旧可以分为简单运算和复合运算两大类进行归类和讲解。
小贴士:为了便于理解,字面量例子我就都使用二进制表示了,使用十进制(任何进制)不影响运算结果
简单运算
简单运算,顾名思义,一次只用一个运算符。
&:按位与
操作规则:同为 1 则 1,否则为 0。仅当两个操作数都为 1 时,输出结果才为 1,否则为 0。
说明:1、本示例(下同)中所有的字面值使用的都是十进制表示的,理解的时候请用二进制思维去理解;2、关于负数之间的位运算本文章统一不做讲述
|:按位或
操作规则:同为 0 则 0,否则为 1。仅当两个操作数都为 0 时,输出的结果才为 0。
~:按位非
操作规则:0 为 1,1 为 0。全部的 0 置为 1,1 置为 0。
小贴士:请务必注意是全部的,别忽略了正数前面的那些 0 哦~
^:按位异或
操作规则:相同为 0,不同为 1。操作数不同时(1 遇上 0,0 遇上 1)对应的输出结果才为 1,否则为 0。
<<:按位左移
操作规则:把一个数的全部位数都向左移动若干位。
左移用得非常多,理解起来并不费劲。x 左移 N 位,效果同十进制里直接乘以 2 的 N 次方就行了,但是需要注意值溢出的情况,使用时稍加注意。
>>:按位右移
操作规则:把一个数的全部位数都向右移动若干位。
负数右移:
右移用得也比较多,也比较理解:操作其实就是把二进制数右边的 N 位直接*砍掉*,然后正数右移高位补0,负数右移高位补1
。
>>>:无符号右移
注意:没有无符号左移,并没有
<<<
这个符号的
它和>>
有符号右移的区别是:无论是正数还是负数,高位通通补 0。所以说对于正数而言,没有区别;那么看看对于负数的表现:
我特意把>>的结果放上面了,方便你对比。因为高位补的是 0,所以就没有显示啦,但是你心里应该清楚是怎么回事。
复合运算
广义上的复合运算指的是多个运算嵌套起来,通常这些运算都是同种类型的。这里指的复合运算指的就是和=号一起来使用,类似于+= -=
。本来这属于基础常识不用做单独解释,但谁让 A 哥管生管养,管杀管埋呢😄。
混合运算:指同一个算式里包含了 bai 多种运算符,如加减乘除乘方开 du 方等。
以 &与运算为例,其它类同:
复习一下&
的运算规则是:同为 1 则 1,否则为 0。
位运算使用场景示例
位运算除了高效的特点,还有一个特点在应用场景下不容忽视:计算的可逆性。通过这个特点我们可以用来达到隐蔽数据的效果,并且还保证了效率。
在 JDK 的原码中。有很多初始值都是通过位运算计算的。最典型的如 HashMap:
位运算有很多优良特性,能够在线性增长的数据中起到作用。且对于一些运算,位运算是最直接、最简便的方法。下面我安排一些具体示例(一般都是面试题),感受一把。
判断两个数字符号是否相同
同为正数 or 同为负数都表示相同,否则为不同。像这种小小 case 用十进制加上>/<
比较符当然可以做,但用位运算符处理来得更加直接(效率最高):
运行程序,输出:
int 类型共 32bit,右移 31 位那么就只剩下 1 个符号位了(因为是带符号右移动,所以正数剩 0 负数剩 1),再对两个符号位做^
异或操作结果为 0 就表明二者一致。
复习一下
^
异或操作规则:相同为 0,不同为 1。
判断一个数的奇偶性
在十进制数中可以通过和 2 取余来做,对于位运算有一个更为高效的方式:
为何&1
能判断基偶性?因为在二进制下偶数的末位肯定是 0,奇数的最低位肯定是 1。
而二进制的 1 它的前 31 位均为 0,所以在和其它数字的前 31 位与运算后肯定所有位数都是 0(无论是 1&0 还是 0&0 结果都是 0),那么唯一区别就是看最低位和 1 进行与运算的结果喽:结果为 1 表示奇数,反则结果为 0 就表示偶数。
交换两个数的值(不借助第三方变量)
这是一个很古老的面试题了,交换 A 和 B 的值。本题如果没有括号里那几个字,是一道大家都会的题目,可以这么来解:
运行程序,输出(成功交换):
使用这种方式最大的好处是:容易理解。最大的坏处是:a+b,可能会超出 int 型的最大范围,造成精度丢失导致错误,造成非常隐蔽的 bug。所以若你这样运用在生产环境的话,是有比较大的安全隐患的。
小贴士:如果你们评估数字绝无可能超过最大值,这种做法尚可。当然如果你是字符串类型,请当我没说
因为这种方式既引入了第三方变量,又存在重大安全隐患。所以本文介绍一种安全的替代方式,借助位运算的可逆性来完成操作:
运行程序,输出(成功完成交换):
由于全文都没有对 a/b 做加法运算,因此不能出现溢出现象,所以是安全的。这种做法的核心原理依据是:位运算的可逆性,使用异或来达成目的。
位运算用在数据库字段上(重要)
这个使用 case 是极具实际应用意义的,因为在生产上我以用过多次,感觉不是一般的好。
业务系统中数据库设计的尴尬现象:通常我们的数据表中可能会包含各种状态属性, 例如 blog 表中,我们需要有字段表示其是否公开,是否有设置密码,是否被管理员封锁,是否被置顶等等。 也会遇到在后期运维中,策划要求增加新的功能而造成你需要增加新的字段,这样会造成后期的维护困难,字段过多,索引增大的情况, 这时使用位运算就可以巧妙的解决。
举个例子:我们在网站上进行认证授权的时候,一般支持多种授权方式,比如:
个人认证 0001 -> 1
邮箱认证 0010 -> 2
微信认证 0100 -> 4
超管认证 1000 -> 8
这样我们就可以使用1111
这四位来表达各自位置的认证与否。要查询通过微信认证的条件语句如下:
要查询既通过了个人认证,又通过了微信认证的:
当然你也可能有排序需求,形如这样:
这种 case 和每个人都熟悉的 Linux 权限控制一样,它就是使用位运算来控制的:权限分为 r 读, w 写, x 执行,其中它们的权值分别为 4,2,1,你可以随意组合授权。比如 chomd 7
,即 7=4+2+1 表明这个用户具有 rwx 权限,
注意事项
需要你的 DB 存储支持位运算,比如 MySql 是支持的
请确保你的字段类型不是 char 字符类型,而应该是数字类型
这种方式它会导致索引失效,但是一般情况下状态值是不需要索引的
具体业务具体分析,别一味地为了 show 而用,若用错了容易遭对有喷的
流水号生成器(订单号生成器)
生成订单流水号,当然这其实这并不是一个很难的功能,最直接的方式就是日期+主机 Id+随机字符串来拼接一个流水号,甚至看到非常多的地方直接使用 UUID,当然这是非常不推荐的。
UUID 是字符串,太长,无序,不能承载有效的信息从而不能给定位问题提供有效帮助,因此一般属于备选方案
今天学了位运算,有个我认为比较优雅方式来实现。什么叫优雅:可以参考淘宝、京东的订单号,看似有规律,实则没规律:
不想把相关信息直接暴露出去。
通过流水号可以快速得到相关业务信息,快速定位问题(这点非常重要,这是 UUID 不建议使用的最重要原因)。
使用 AtomicInteger 可提高并发量,降低了冲突(这是不使用 UUID 另一重要原因,因为数字的效率比字符串高)
实现原理简介
此流水号构成:日期+Long 类型的值 组成的一个一长串数字,形如2020010419492195304210432
。很显然前面是日期数据,后面的一长串就蕴含了不少的含义:当前秒数、商家 ID(也可以是你其余的业务数据)、机器 ID、一串随机码等等。
各部分介绍:
第一部分为当前时间的毫秒值。最大 999,所以占 10 位
第二部分为:serviceType 表示业务类型。比如订单号、操作流水号、消费流水号等等。最大值定为 30,足够用了吧。占 5 位
第三部分为:shortParam,表示用户自定义的短参数。可以放置比如订单类型、操作类型等等类别参数。最大值定为 30,肯定也是足够用了的。占 5 位
第四部分为:longParam,同上。用户一般可放置 id 参数,如用户 id、商家 id 等等,最大支持 9.9999 亿。绝大多数足够用了,占 30 位
第五部分:剩余的位数交给随机数,随机生成一个数,占满剩余位数。一般至少有 15 位剩余(此部分位数是浮动的),所以能支持 2 的 15 次方的并发,也是足够用了的
最后,在上面的 long 值前面加上日期时间(年月日时分秒)
这是 A 哥编写的一个基于位运算实现的流水号生成工具,已用于生产环境。考虑到源码较长(一个文件,共 200 行左右,无任何其它依赖)就不贴了,若有需要,**请到公众号后台回复流水号生成器
免费获取**。
✍总结
位运算在工程的角度里缺点还是蛮多的,在实际工作中,如果只是为了数字的计算,是不建议使用位运算符的,只有一些比较特殊的场景,使用位运算去做会给你柳暗花明的感觉,如:
N 多状态的控制,需要兼具扩展性。比如数据库是否状态的字段设计
对效率有极致要求。比如 JDK
场景非常适合。比如 Jackson 的 Feature 特针值
切忌为了炫(zhuang)技(bi)而使用,炫技一时爽,掉坑火葬场;小伙还年轻,还望你谨慎。代码在大多情况下,人能容易读懂比机器能读懂来得更重要。
✔推荐阅读:
---------
♥关注 A 哥♥
Author | A哥(YourBatman)
-------- | -----
个人站点 | www.yourbatman.cn
E-mail | yourbatman@qq.com
微 信 | fsx641385712
公众号 | BAT的乌托邦(ID:BAT-utopia)
知识星球 | BAT的乌托邦
每日文章推荐 | 每日文章推荐
版权声明: 本文为 InfoQ 作者【YourBatman】的原创文章。
原文链接:【http://xie.infoq.cn/article/6cd52956d6a50f8f92e68e7b6】。文章转载请联系作者。
评论