基礎不牢,地動山搖。本文已被 https://www.yourbatman.cn 收錄,裏面一併有Spring技術棧、MyBatis、JVM、中間件等小而美的專欄供以避免費學習。關注公衆號【BAT的烏托邦】逐個擊破,深刻掌握,拒絕淺嘗輒止。java
你好,我是YourBatman。程序員
本號正在連載Jackson深度解析系列,雖然目前還只講到了其流式API層面,但已接觸到其多個Feature
特徵。更爲重要的是我在文章裏贊其設計精妙,處理優雅,所以就有小夥伴私信給我問這樣的話:
題外話:Jackson這個話題本就很是小衆,看着閱讀量我本身都快沒信心寫下去。但本身說過的話就是欠下的債,熬夜也得把承諾的付費內容給公開完了,畢竟還有那麼幾我的在白嫖不是😄。面試
話外音:之後悶頭作事,少吹牛逼┭┮﹏┭┮算法
雖然小衆,居然還有想深刻了解一波的小夥伴,確實讓我爲之振奮了那麼三秒。既然如此那就幹吧,本文就先行來認識認識Java中的位運算。位運算在Java中不多被使用,那麼爲什麼Jackson裏愛不釋手呢?一切就爲兩字:性能/高效。用計算機能直接看懂的語言跟它打交道,你說快不快,不用多想嘛。
sql
說起位運算,對絕大多數Java程序員來講,是一種既熟悉又陌生的感受。熟悉是由於你在學JavaSE時確定學過,而且在看一些開源框架(特別是JDK源碼)時都能看到它的身影;陌生是由於大機率咱們不會去使用它。固然,不能「流行」起來是有緣由的:很差理解,不符合人類的思惟,閱讀性差…...數據庫
小貼士:通常來講,程序讓人看懂遠比被機器看懂來得更重要些json
位運算它在low-level
的語言裏使用得比較多,可是對於Java這種高級語言它就不多被說起了。雖然咱們使用得不多但Java也是支持的,畢竟不少時候使用位運算纔是最佳實踐。api
位運算在平常開發中使用得較少,可是巧妙的使用位運算能夠大量減小運行開銷,優化算法。一條語句可能對代碼沒什麼影響,可是在高重複,大數據量的狀況下將會節省不少開銷。安全
在瞭解什麼是位運算以前,十分有必要先科普下二進制的概念。微信
二進制是計算技術中普遍採用的一種數制。二進制數據是用0和1兩個數碼來表示的數。它的基數爲2,進位規則是逢二進一,借位規則是借一當二。由於它只使用0、1兩個數字符號,很是簡單方便,易於用電子方式實現。
小貼士:半導體開表明1,關表明0,這也就是CPU計算的最底層原理😄
先看一個例子:
求 1011(二進制)+ 11(二進制) 的和? 結果爲:1110(二進制)
二進制理解起來很是很是的簡單,比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字節)。
在Java7版本之前,Java是不支持直接書寫除十進制之外的其它進制字面量。但這在Java7以及之後版本就容許了:
@Test public void test1() { //二進制 int i = 0B101; System.out.println(i); //5 System.out.println(Integer.toBinaryString(i)); //八進制 i = 0101; System.out.println(i); //65 System.out.println(Integer.toBinaryString(i)); //十進制 i = 101; System.out.println(i); //101 System.out.println(Integer.toBinaryString(i)); //十六進制 i = 0x101; System.out.println(i); //257 System.out.println(Integer.toBinaryString(i)); }
結果程序,輸出:
5 101 65 1000001 101 1100101 257 100000001
說明:System.out.println()
會先自動轉爲10進制後再輸出的;toBinaryString()
表示轉換爲二進制進行字符串進行輸出。
JDK自1.0
開始便提供了很是便捷的進制轉換的API,這在咱們有須要時很是有用。
@Test public void test2() { int i = 192; System.out.println("---------------------------------"); System.out.println("十進制轉二進制:" + Integer.toBinaryString(i)); //11000000 System.out.println("十進制轉八進制:" + Integer.toOctalString(i)); //300 System.out.println("十進制轉十六進制:" + Integer.toHexString(i)); //c0 System.out.println("---------------------------------"); // 統一利用的爲Integer的valueOf()方法,parseInt方法也是ok的 System.out.println("二進制轉十進制:" + Integer.valueOf("11000000", 2).toString()); //192 System.out.println("八進制轉十進制:" + Integer.valueOf("300", 8).toString()); //192 System.out.println("十六進制轉十進制:" + Integer.valueOf("c0", 16).toString()); //192 System.out.println("---------------------------------"); }
運行程序,輸出:
--------------------------------- 十進制轉二進制:11000000 十進制轉八進制:300 十進制轉十六進制:c0 --------------------------------- 二進制轉十進制:192 八進制轉十進制:192 十六進制轉十進制:192 ---------------------------------
我相信每一個Javaer都知道Java中的Long類型佔8個字節(64位),那如何證實呢?
小貼士:這算是一道經典面試題,至少我提問過屢次~
有個最簡單的方法:拿到Long類型的最大值,用2進製表示轉換成字符串看看長度就好了,代碼以下:
@Test public void test3() { long l = 100L; //若是不是最大值 前面都是0 輸出的時候就不會有那麼長了(因此下面使用最大/最小值示例) System.out.println(Long.toBinaryString(l)); //1100100 System.out.println(Long.toBinaryString(l).length()); //7 System.out.println("---------------------------------------"); l = Long.MAX_VALUE; // 2的63次方 - 1 //正數長度爲63爲(首位爲符號位,0表明正數,省略了因此長度是63) //111111111111111111111111111111111111111111111111111111111111111 System.out.println(Long.toBinaryString(l)); System.out.println(Long.toBinaryString(l).length()); //63 System.out.println("---------------------------------------"); l = Long.MIN_VALUE; // -2的63次方 //負數長度爲64位(首位爲符號位,1表明負數) //1000000000000000000000000000000000000000000000000000000000000000 System.out.println(Long.toBinaryString(l)); System.out.println(Long.toBinaryString(l).length()); //64 }
運行程序,輸出:
1100100 7 --------------------------------------- 111111111111111111111111111111111111111111111111111111111111111 63 --------------------------------------- 1000000000000000000000000000000000000000000000000000000000000000 64
說明:在計算機中,負數以其正值的補碼的形式表達。所以,用一樣的方法你能夠自行證實Integer類型是32位的(佔4個字節)。
Java語言支持的位運算符仍是很是多的,列出以下:
&
:按位與|
:按位或~
:按位非^
:按位異或<<
:左位移運算符>>
:右位移運算符>>>
:無符號右移運算符除~
以 外,其他均爲二元運算符,操做的數據只能是整型(長短都可)或者char字符型。針對這些運算類型,下面分別給出示例,一目瞭然。
既然是運算,依舊能夠分爲簡單運算和複合運算兩大類進行歸類和講解。
小貼士:爲了便於理解,字面量例子我就都使用二進制表示了,使用十進制(任何進制)不影響運算結果
簡單運算,顧名思義,一次只用一個運算符。
操做規則:同爲1則1,不然爲0。僅當兩個操做數都爲1時,輸出結果才爲1,不然爲0。
說明:一、本示例(下同)中全部的字面值使用的都是十進制表示的,理解的時候請用二進制思惟去理解;二、關於負數之間的位運算本文章統一不作講述
@Test public void test() { int i = 0B100; // 十進制爲4 int j = 0B101; // 十進制爲5 // 二進制結果:100 // 十進制結果:4 System.out.println("二進制結果:" + Integer.toBinaryString(i & j)); System.out.println("十進制結果:" + (i & j)); }
操做規則:同爲0則0,不然爲1。僅當兩個操做數都爲0時,輸出的結果才爲0。
@Test public void test() { int i = 0B100; // 十進制爲4 int j = 0B101; // 十進制爲5 // 二進制結果:101 // 十進制結果:5 System.out.println("二進制結果:" + Integer.toBinaryString(i | j)); System.out.println("十進制結果:" + (i | j)); }
操做規則:0爲1,1爲0。所有的0置爲1,1置爲0。
小貼士:請務必注意是所有的,別忽略了正數前面的那些0哦~
@Test public void test() { int i = 0B100; // 十進制爲4 // 二進制結果:11111111111111111111111111111011 // 十進制結果:-5 System.out.println("二進制結果:" + Integer.toBinaryString(~i)); System.out.println("十進制結果:" + (~i)); }
操做規則:相同爲0,不一樣爲1。操做數不一樣時(1趕上0,0趕上1)對應的輸出結果才爲1,不然爲0。
@Test public void test() { int i = 0B100; // 十進制爲4 int j = 0B101; // 十進制爲5 // 二進制結果:1 // 十進制結果:1 System.out.println("二進制結果:" + Integer.toBinaryString(i ^ j)); System.out.println("十進制結果:" + (i ^ j)); }
操做規則:把一個數的所有位數都向左移動若干位。
@Test public void test() { int i = 0B100; // 十進制爲4 // 二進制結果:100000 // 十進制結果:32 = 4 * (2的3次方) System.out.println("二進制結果:" + Integer.toBinaryString(i << 2)); System.out.println("十進制結果:" + (i << 3)); }
左移用得很是多,理解起來並不費勁。x左移N位,效果同十進制裏直接乘以2的N次方就好了,可是須要注意值溢出的狀況,使用時稍加註意。
操做規則:把一個數的所有位數都向右移動若干位。
@Test public void test() { int i = 0B100; // 十進制爲4 // 二進制結果:10 // 十進制結果:2 System.out.println("二進制結果:" + Integer.toBinaryString(i >> 1)); System.out.println("十進制結果:" + (i >> 1)); }
負數右移:
@Test public void test() { int i = -0B100; // 十進制爲-4 // 二進制結果:11111111111111111111111111111110 // 十進制結果:-2 System.out.println("二進制結果:" + Integer.toBinaryString(i >> 1)); System.out.println("十進制結果:" + (i >> 1)); }
右移用得也比較多,也比較理解:操做其實就是把二進制數右邊的N位直接砍掉,而後正數右移高位補0,負數右移高位補1
。
注意:沒有無符號左移,並無
<<<
這個符號的
它和>>
有符號右移的區別是:不管是正數仍是負數,高位統統補0。因此說對於正數而言,沒有區別;那麼看看對於負數的表現:
@Test public void test() { int i = -0B100; // 十進制爲-4 // 二進制結果:11111111111111111111111111111110(>>的結果) // 二進制結果:1111111111111111111111111111110(>>>的結果) // 十進制結果:2147483646 System.out.println("二進制結果:" + Integer.toBinaryString(i >>> 1)); System.out.println("十進制結果:" + (i >>> 1)); }
我特地把>>的結果放上面了,方便你對比。由於高位補的是0,因此就沒有顯示啦,可是你內心應該清楚是怎麼回事。
廣義上的複合運算指的是多個運算嵌套起來,一般這些運算都是同種類型的。這裏指的複合運算指的就是和=號一塊兒來使用,相似於+= -=
。原本這屬於基礎常識不用作單獨解釋,但誰讓A哥管生管養,管殺管埋呢😄。
混合運算:指同一個算式裏包含了bai多種運算符,如加減乘除乘方開du方等。
以&與運算爲例,其它類同:
@Test public void test() { int i = 0B110; // 十進制爲6 i &= 0B11; // 效果同:i = i & 3 // 二進制結果:10 // 十進制結果:2 System.out.println("二進制結果:" + Integer.toBinaryString(i)); System.out.println("十進制結果:" + (i)); }
複習一下&
的運算規則是:同爲1則1,不然爲0。
位運算除了高效的特色,還有一個特色在應用場景下不容忽視:計算的可逆性。經過這個特色咱們能夠用來達到隱蔽數據的效果,而且還保證了效率。
在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表中,咱們須要有字段表示其是否公開,是否有設置密碼,是否被管理員封鎖,是否被置頂等等。 也會遇到在後期運維中,策劃要求增長新的功能而形成你須要增長新的字段,這樣會形成後期的維護困難,字段過多,索引增大的狀況, 這時使用位運算就能夠巧妙的解決。
舉個例子:咱們在網站上進行認證受權的時候,通常支持多種受權方式,好比:
這樣咱們就可使用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權限,
生成訂單流水號,固然這其實這並非一個很難的功能,最直接的方式就是日期+主機Id+隨機字符串來拼接一個流水號,甚至看到很是多的地方直接使用UUID,固然這是很是不推薦的。
UUID是字符串,太長,無序,不能承載有效的信息從而不能給定位問題提供有效幫助,所以通常屬於備選方案
今天學了位運算,有個我認爲比較優雅方式來實現。什麼叫優雅:能夠參考淘寶、京東的訂單號,看似有規律,實則沒規律:
此流水號構成:日期+Long類型的值 組成的一個一長串數字,形如2020010419492195304210432
。很顯然前面是日期數據,後面的一長串就蘊含了很多的含義:當前秒數、商家ID(也能夠是你其他的業務數據)、機器ID、一串隨機碼等等。
各部分介紹:
這是A哥編寫的一個基於位運算實現的流水號生成工具,已用於生產環境。考慮到源碼較長(一個文件,共200行左右,無任何其它依賴)就不貼了,如有須要,請到公衆號後臺回覆流水號生成器
免費獲取。
位運算在工程的角度裏缺點仍是蠻多的,在實際工做中,若是隻是爲了數字的計算,是不建議使用位運算符的,只有一些比較特殊的場景,使用位運算去作會給你柳暗花明的感受,如:
切忌爲了炫(zhuang)技(bi)而使用,炫技一時爽,掉坑火葬場;小夥還年輕,還望你謹慎。代碼在大多狀況下,人能容易讀懂比機器能讀懂來得更重要。