JAVA有關位運算的全套梳理

1、在計算機中數據是如何進行計算的?

1.1:java中的byte型數據取值範圍

咱們最開始學習java的時候知道,byte類型的數據佔了8個bit位,每一個位上或0或1,左邊第一位表示符號位,符號位若是爲1表示負數,爲0則表示正數,所以要推算byte的取值範圍,只須要讓數值位每一位上都等於1便可。java

咱們來用咱們的常規思惟來分析下byte類型的取值範圍:數據庫

圖1性能

若是按照這種思路來推算,七個1的二進制數轉換爲十進制是127,算上符號位,取值範圍應爲:-127~+127,但事實上咱們知道,byte的取值範圍是-128~127,這裏先打個問號,接着往下看。學習

如今讓咱們計算下byte類型的7加上byte類型的-2是多少:spa

圖23d

誒?跟咱們預想的不同,由於咱們是知道7和-2的和應該是5纔對,結果應該表示爲:00000101,但事實上經過圖2的結果來看確實跟預想的不同,因此計算機在作計算的時候,確定不是表面上的符號位+數值位的方式進行的計算的。code

1.2:原碼,反碼,補碼

咱們先來看下定義:blog

👉 原碼定義:符號位加後面的數值,好比圖2裏的00000111和10000010都是原碼,原碼比較簡單,就是咱們在上面單純理解上的原值。it

👉 反碼定義:正數的反碼就是它的原碼,負數的反碼符號位不變,其他數值位所有按位取反,例如:class

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值呢?

即:1000000000000000,+0和-0;

其次若是站在機器角度上來講,全部的負數都很大,至少要比全部正數大,由於負數的最高位也就是符號位都是1,顯然這是不對的,經過本節咱們知道了,全部的數均經過本身的補碼完成計算,若是將最後獲得的結果轉成原碼,就是咱們人眼能夠理解的最終值(符號位+數值位),若是如今利用補碼的方式作理解,符號位爲0的數沒啥好說的,天然取值區間爲:0~127,可是符號位爲1的負數呢?負數就存在一個特殊值(也就是咱們以前片面理解的-0):10000000,若是按照原碼理解它是-0,但咱們前面說過,計算機裏全部數字,都是以補碼的方式參與運算的,而負數的補碼不等於其原碼,這個10000000在計算機裏顯然是某個負數的補碼,那麼問題就變的簡單多了,即10000000是誰的補碼呢?答案是:-128,這也是爲何負數的取值範圍會比正數多一個的緣由,byte類型如此,其它類型也是如此,好比int型的負數取值也比正數多1。

這一塊的定義要清晰,對理解後面的位運算會有很大的幫助。

2、java中的位運算

2.1:與運算

與運算符號:&

與運算特色:1&1=一、1&0=0、0&1=0、0&0=0

如今咱們來舉一個例子:

圖6

 

讓咱們再來試試負數:

圖7

2.2:或、異或

跟與運算的運算方式一致,只不過規則不太同樣:

或運算符號:|

或運算規則:1|1=一、1|0=一、0|1=一、0|0=0

異或運算符號:^

異或運算規則:1^1=0、1^0=一、0^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

3、位運算在實際項目中的運用

位運算的性能是很是好的,相比運算流程,計算機更喜歡這種純粹的邏輯門和移動位置的運算,但位運算在日常的業務代碼裏並不太常見,由於它的可讀性不太好,可是咱們仍然能夠利用位運算來解決一些實際項目裏的問題。

好比用來表示開關的功能,好比需求裏常常有這種字段:是否容許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型~

相關文章
相關標籤/搜索