JAVA位運算的知識點總結
一、在計算機中資料是如何進行計算的?
1.1:java中的byte型資料取值範圍
我們最開始學習java的時候知道,byte型別的資料佔了8個bit位,每個位上或0或1,左邊第一位表示符號位,符號位如果為1表示負數,為0則表示正數,因此要推算byte的取值範圍,只需要讓數值位每一位上都等於1即可。
我們來用我們的常規思維來分析下byte型別的取值範圍:
圖1
如果按照這種思路來推算,七個1的二進位制數轉換為十進位制是127,算上符號位,取值範圍應為:-127~+127,但事實上我們知道,byte的取值範圍是-128~127,這裡先打個問號,接著往下看。
現在讓我們計算下byte型別的7加上byte型別的-2是多少:
圖2
誒?跟我們預想的不一樣,因為我們是知道7和-2的和應該是5才對,結果應該表示為:00000101,但事實上通過圖2的結果來看確實跟預想的不一樣,所以計算機在做計算的時候,肯定不是表面上的符號位+數值位的方式進行的計算的。
1.2:原碼,反碼,補碼
我們先來看下定義:
👉 原碼定義:符號位加後面的數值,比如圖2裡的00000111和10000010都是原碼,原碼比較簡單,就是我們在上面單純理解上的原值。
👉 反碼定義:正數的反碼就是它的原碼,負數的反碼符號位不變,其餘數值位全部按位取反,例如:
00000111的反碼:00000111
10000010的反碼:11111101
👉 補碼定義:同樣的,正數的補碼仍然等於它的原碼本身,負數的補碼等於它自己的反碼+1,例如:
00000111的補碼:00000111
10000010的補碼:11111110
🌴 總結:正數的原碼、反碼、補碼完全一致,負數的反碼等於它原碼的數值位按位取反,負數的補碼等於它的反碼+1
現在讓我們用反碼的方式來計算下圖2中的式子:
圖3
利用數值的反碼計算出的結果已經很接近正確答案了,+4的反碼等於它的原碼,現在只需要讓它+1就是正確答案,還記得補碼的定義嗎?負數的補碼等於它的反碼+1,那現在讓我們用補碼做下計算試試?
圖4
ok,我們發現,用它們的補碼做加法,得到的數值就是我們想要的正確答案,事實上,計算機並沒有減法運算器,所有的減法運算,都是以一個正數加上一個負數的形式來交給加法運算器計算的,由於負數的符號位為1,雖然我們人是知道它的含義,但是作為計算機,它是不知道第一位是符號位的,它要做的就僅僅是讓兩個數相加而已,正是因為如此,我們才不能簡簡單單儲存負數,通過圖4我們知道,兩個數的補碼相加,可以得到一個準確的數值。
再舉個相加結果為負數的例子,讓兩個負數相加:
圖5
如果結果為負數的話,也是適用的,只是它仍然是以補碼的形式存放的,需要轉成原碼才符合我們人的理解方式。
現在回到上面留下的問題,為什麼byte的取值範圍是-128~127呢?
我們之前按照圖1裡的理解,理所應當的以為它應該是-127~127的範圍,那是因為我們按照圖1的理解方式,數值就是以符號位+數值位的方式理解的(也就是按照原碼的方式理解的),但是你可以想一下,如果按照圖1那種理解方式,是不是會存在兩個0值呢?
即:10000000和00000000,+0和-0;
其次如果站在機器角度上來說,所有的負數都很大,至少要比所有正數大,因為負數的最高位也就是符號位都是1,顯然這是不對的,通過本節我們知道了,所有的數均通過自己的補碼完成計算,如果將最後得到的結果轉成原碼,就是我們人眼可以理解的最終值(符號位+數值位),如果現在利用補碼的方式做理解,符號位為0的數沒啥好說的,自然取值區間為:0~127,但是符號位為1的負數呢?負數就存在一個特殊值(也就是我們之前片面理解的-0):10000000,如果按照原碼理解它是-0,但我們前面說過,計算機裡所有數字,都是以補碼的方式參與運算的,而負數的補碼不等於其原碼,這個10000000在計算機裡顯然是某個負數的補碼,那麼問題就變的簡單多了,即10000000是誰的補碼呢?答案是:-128,這也是為什麼負數的取值範圍會比正數多一個的原因,byte型別如此,其它型別也是如此,比如int型的負數取值也比正數多1。
這一塊的定義要清晰,對理解後面的位運算會有很大的幫助。
二、java中的位運算 2.1:與運算
與運算子號:&
與運算特點:1&1=1、1&0=0、0&1=0、0&0=0
現在我們來舉一個例子:
圖6
讓我們再來試試負數:
圖7
2.2:或、異或
跟與運算的運算方式一致,只不過規則不太一樣:
或運算子號:|
或運算規則:1|1=1、1|0=1、0|1=1、0|0=0
異或運算子號:^
異或運算規則:1^1=0、1^0=1、0^1=1、0^0=0
2.3:按位取反
取反符號:~
即一個數對自己取反,例如:
某個數字a的二進位制為: 1010110
則~a為: 0101001
2.4:左移運算
左移運算子:<<
例如:
圖8
位運算越界&數位拋棄:
圖8中的116的二進位制數的數值位為7位,符號位為0,此時如果左移超過24位,就會出現負數,為什麼會這樣?因為java中的位移越界時,java會拋棄高位越界部分,我們知道java裡int型別的第一位是符號位,如果符號位是1,則表示其為負數,現在將數值位佔7bit符號位為0的116左移24位,就會出現下方結果:
01110100000000000000000000000000
正好31位佔全,頂至符號位,低位補0,我們稱24為116的不越界的最大左移值,若超出這個值,就會越界,比如左移25位:
11101000000000000000000000000000
顯然左移25位後會把數值位的1移動到符號位,這時它表示為一個負數的補碼。根據這個規則,我們如果讓其左移28位,則值為:
01000000000000000000000000000000
也就是十進位制的1073741824,即:116 << 28 = 1073741824,那如果越界過多呢?比如int型的資料,左移32位:116 << 32 = 116
會發現,如果左移自己位數一樣多的位數,那麼這個數就等於它本身,因此運算符合以下規則:
設x為被位移值,y為本次位移的位數,z為x所屬型別的最大儲存位數:
x << y = x << (y%z)
如果是int型(32位,long型就用64代入計算),符合如下規則:
116 << 4=116 << (4%32)=116 << 4=1856
116 << 32=116 << (32%32)=116 << 0=116
116 << 36=116 << (36%32)=116 << 4=1856
2.5:有符號右移運算&無符號右移運算
有符號右移運算子:>>
無符號右移運算子:>>>
例如:a >> b表示a右移b位,跟上面的左移例子一樣,右移也會有越界問題,只是右移越界是從右邊開始拋棄越界部分的,右移操作有符號位干擾,如果是正數右移,無此干擾項,因為符號位本就是0右移不會影響值的準確性,但如果是負數,第一位是符號位,且值為1,右移就有影響了,現在仍然以116為例:
正數右移:
圖9
上述是正數,右移無影響,但是負數,這裡以-116為例,我們知道負數在計算機裡是以補碼的形式儲存的,所以圖裡直接用-116的補碼做運算,位移過程如下:
圖10
你會發現右移跟左移不一樣,左移是不用擔心自己符號位存在“補位”問題的,但是右移存在,如圖中-116右移4位後,左邊第一位,也就是符號位,就面臨著補位的問題,那我現在是該補1呢,還是補0呢?這也就是為什麼右移操作會存在有符號右移和無符號右移兩種移動方式:
☘️ 有符號右移:依照原符號位,如果原符號位是1,那麼圖4裡需要補位的空位全部補1,如果原符號位為0,則全部補0
☘️ 無符號右移:無視原符號位,全部補0
現在讓我們用有符號的方式將-116右移4位,即-116 >> 4,按照有符號的規則,補位符合原符號位,則右邊4位全部補1:
圖11
得到的仍然是個負數,它仍然是一個補碼,圖裡展示不開,它的結果為:11111111111111111111111111111000,經轉換可知它是-8的補碼,即:-116 >> 4 = -8
現在再試試用無符號右移,根據無符號的特點,右移後的前四位無腦補0:
圖12
圖裡展示不開,它的結果為:00001111111111111111111111111000
可見它是個正數,轉換成十進位制為:268435448,即:-116 >>> 4 = 268435448
最後說一下,跟左移一樣,右移裡不管是有符號還是無符號,也符合取餘的方式,計算出位移的最終位數:
-116 >> 4=-116 >> (4%32)=-116 >> 4=-8
-116 >> 32=-116 >> (32%32)=-116 >> 0=-116
-116 >> 36=-116 >> (36%32)=-116 >> 4=-8
2.6:型別轉換溢位
瞭解完位運算,來看一個比較實際的問題,看下面的程式碼:
long a = 8934567890233345621L; int b = (int) a; //b的值為-1493678507
最終b的值是一個負數,這是由於long型64位,讓int型強行接收,會出現位溢位的問題,這個流程如下:
圖13
三、位運算在實際專案中的運用
位運算的效能是非常好的,相比運算流程,計算機更喜歡這種純粹的邏輯閘和移動位置的運算,但位運算在平常的業務程式碼裡並不太常見,因為它的可讀性不太好,但是我們仍然可以利用位運算來解決一些實際專案裡的問題。
比如用來表示開關的功能,比如需求裡經常有這種欄位:是否允許xx(0不允許,1允許),是否有yy許可權(0沒有,1有),是否存在zz(0不存在,1存在)
上面只是舉例,類似這種只有兩種取值狀態的屬性,如果當成資料庫欄位放進去的話,太過浪費,如果之後又有類似的欄位,又得新增資料庫欄位,為了只有兩種取值的欄位,實在是不太值得。
這個時候何不用一個欄位來表示這些欄位呢?你可能已經猜到要怎麼做了:
圖14
頂一個int型或者long型的欄位,讓它的每一個二進位制位擁有特殊含義即可,然後按照位運算將其對應的位置上的數值變成0或1,那如何將某個數的二進位制位第x位上的數值變成1或0呢?其實這在點陣圖結構裡經常用到,就是利用1這個特殊的值作位移運算後再與原值進行位運算,讓我們看下這個過程:
把一個數的第2位的字元變成1,現在假設這個數初始化為0,int型,我們把它當成二進位制展示出來:
圖15
現在如何把這個數的第二位變成1呢?目前是這樣做的:
0 | 1 << 1
即原值跟1左移1位後的值作或運算,先來看看1 << 1的結果:
圖16
然後拿著圖16的結果,跟原數(也就是0)進行或運算:
圖17
可以看到,原數的第二位已經被置為1了,它的十進位制對應2,其它位的數置為1也大同小異,例如,現在讓第6位也變成1只需要:
2 | 1 << 5
即拿著原值(現在為2)跟1左移5位後的數做或運算,這個流程如下:
圖18
看完了把某個位置的數值置為1,那如何把某位設定為0呢?我們現在把圖18裡的結果的第6位重新置回0,目前的做法為:
34 & ~(1 << 5)
即拿著原值(經過上面幾步的運算,現在值為32)跟1左移5位按位取反後的數做與運算,來看下這個流程:
圖19
經過上面的流程,就可以把原值的第6位變成0了。
那麼我們知道了讓一個數的二進位制位的某位變成0或1的方法,那如何知道一個數的某位上究竟是0還是1呢?畢竟我們業務程式碼需要知道第幾位代表什麼意思並且獲取到對應位置上的值。
假如我現在想知道十進位制int型數34的第6位是0還是1,寫法如下:
34 >> 5 & 1
即讓原值(34)右移5位後跟1做與運算,來看下這個流程:
圖20
由圖可以看出,想要知道一個數的第幾位是1還是0,只需要將其對應位置上的值“逼”到最後一位,然後跟1相與即可,如果對應位置上的值是0,那麼與1相與後的結果一定為0,反之一定為1.
☘️總結
到這裡已經說完了為什麼要用一個數表示那麼多開關,以及如何給一個開關位設定對應的開關值,以及如何找到對應開關位的值,有了這些操作,我們再也不需要為這種只有0和1取值的欄位新增資料庫欄位了,因為一個int型的數字,就可以表達32個開關屬性,如果超了,還可以擴成64位的long型~
到此這篇關於JAVA位運算的知識點總結的文章就介紹到這了,更多相關JAVA有關位運算的全套梳理內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!