折騰的心,顫抖的手,只因在 main 函數(shù)中執(zhí)行了一次 int 強(qiáng)轉(zhuǎn) byte 的操作,輸出結(jié)果太出所料,于是入坑,鉆研良久,遂有此篇。

            我們都知道,Java中有8中基本數(shù)據(jù)類型,每種類型都有取值范圍,比如 1 個字節(jié)的 byte 取值范圍是【-128~127】,4 個字節(jié)的 int
          取值范圍是 【-231~231-1】。因?yàn)槟鼙硎镜闹档姆秶煌?,如果我們?int 類型強(qiáng)轉(zhuǎn)為 byte 類型的話,是很可能損失精度的,比如:
          byte a = (byte) 127; // a = 127 byte b = (byte) 128; // b = -128 byte c = (byte
          ) 256;// c = 0
            以人腦的主觀意識,128 只比 byte 范圍上限多 1 而已,如果損失精度,把多的 1 舍去變 127 就好了啊,怎么就變成了
          -128?也曾看到一些園友說,這種強(qiáng)轉(zhuǎn)造成的精度損失后的結(jié)果是毫無意義的,博主不以為然;稍微深究一下,你會發(fā)現(xiàn),這結(jié)果并不是編譯器隨便給的數(shù)字,而是經(jīng)過邏輯運(yùn)算后的結(jié)果——雖然,表面上你看不出這種結(jié)果有什么運(yùn)算邏輯,但你不要忘了,計(jì)算機(jī)的邏輯都是二進(jìn)制邏輯,不是你我的人腦邏輯啊。

          二進(jìn)制

            二進(jìn)制,是計(jì)算機(jī)唯一能識別、存儲的數(shù),用0和1兩個數(shù)碼來表示,基數(shù)為2,“逢二進(jìn)一”,”借一當(dāng)二”。

            要搞清楚上面 Java
          代碼的運(yùn)算邏輯,我們首先要做的是將對我們?nèi)四X直觀的十進(jìn)制數(shù)字轉(zhuǎn)換成對計(jì)算機(jī)直觀的二進(jìn)制,這里就用到了一個概念叫比特位(bit),這是計(jì)算機(jī)最小的存儲單元了,表示二進(jìn)制的存儲位。而我們說
          一個字節(jié)占用 8 個長度位,就是指一個字節(jié)占用了八個比特位的長度,也就是八個二進(jìn)制位。布衣博主
          畫了一份草圖,來將上文中的十進(jìn)制數(shù)轉(zhuǎn)換成二進(jìn)制比特存儲位,這里先以十進(jìn)制的?256?為例:



          ?


            將4字節(jié)的int類型數(shù)據(jù)轉(zhuǎn)換成單字節(jié)的byte,最高位的三個字節(jié)的存儲單元將被舍棄掉,這才是損失精度的要義所在!所以,根據(jù)上圖高位舍棄的強(qiáng)轉(zhuǎn)后,你自己也可以看出來,最后得到的
          byte 十進(jìn)制表示數(shù)字 0 。嗯,似乎也就那么回事,還是很好理解,但是,沿用上面的圖,我們換成128 試試?

          ?

          ?

          ?  看草圖,似乎也很簡單,128強(qiáng)轉(zhuǎn)后,按照高位舍棄理論,無非是舍棄掉了高字節(jié)位無意義的 24 個 0 而已,最后的 byte
          字節(jié)表示的還是原來那么大,還應(yīng)該是 128 才對啊,為什么實(shí)際程序運(yùn)行的結(jié)果卻變成了 -128 ?
          咳咳!老師有沒有告訴過你,Java的數(shù)據(jù)是帶符號的?你知道二進(jìn)制中如何表示一個數(shù)的正負(fù)的嗎?所以,上訴理論中,我們還遺漏了一個很重要的知識點(diǎn),那就是符號位
          的表示。對于有符號二進(jìn)制來說,為了區(qū)分?jǐn)?shù)的正負(fù),約定以最高位作為符號位,0表示正數(shù),1 表示負(fù)數(shù),除去符號位剩下的就是這個數(shù)的絕對值部分:



            我們帶上符號位,回過頭來重新分析上面對 128 的強(qiáng)轉(zhuǎn):當(dāng)高位的三個字節(jié)被舍棄掉之后,連同舍棄的還有它的符號位 0
          ,最終的結(jié)果就是強(qiáng)轉(zhuǎn)成單字節(jié)后,原來表示數(shù)值部分的 1 變成了符號位,表示為負(fù),除去符號位,能表示值的就只有后7位的 0000000
          了。這樣表示的十進(jìn)制值為? -0,在帶符號的二進(jìn)制中,-0 被規(guī)定用來指代 -128,+0 才表示 0
          。看來,只要帶上符號位,本文最開始的輸出結(jié)果是很好分析的。至此,我們引出了二進(jìn)制中的符號位,并用此解答了本文一開始的疑惑。但是,有了符號位,這里又有疑問了,如果符號位占據(jù)了字節(jié)高位(第一位),當(dāng)我們在進(jìn)行算數(shù)運(yùn)算的時候,符號位又該如何處理呢?

          原碼、反碼和補(bǔ)碼


            雖然人類用計(jì)算器算賬的時候,是按十進(jìn)制的思維來進(jìn)行加減乘除的,但是對于計(jì)算機(jī)來說,它最終會將人類的輸入轉(zhuǎn)化成底層的二進(jìn)制來操作。如果我們的二進(jìn)制操作都是帶符號的,這就會有上面提到的符號位該如何處理的問題。比如
          5-3,21-25,21+15,-3-8
          等等,兩個數(shù)字相加減,結(jié)果可能是正數(shù)也可能是負(fù)數(shù),如果將符號位加入運(yùn)算,如何進(jìn)行進(jìn)位和借位操作?如果符號位不加入運(yùn)算,單獨(dú)區(qū)分符號位肯定會增加計(jì)算機(jī)底層設(shè)計(jì)的復(fù)雜度。不管怎樣,有一點(diǎn)可以肯定,那就是帶符號的二進(jìn)制數(shù)是不能直接拿來運(yùn)算的!腫么辦呢?為了不增加計(jì)算機(jī)底層設(shè)計(jì)的復(fù)雜度,人類還是決定在符號位上下功夫,于是有了我們熟知的二進(jìn)制領(lǐng)域中的?
          原碼,反碼以及補(bǔ)碼等等概念,下面是三種碼基本的表示的方法:

          *   原碼:符號位(字節(jié)序列的最高位)加上原數(shù)值絕對值的二進(jìn)制表示;
          *   反碼:正數(shù)的反碼是其本身,負(fù)數(shù)的反碼為保持符號位不變其余位置按位取反;
          *   補(bǔ)碼:正數(shù)補(bǔ)碼依舊是其本身,負(fù)數(shù)補(bǔ)碼為反碼加1;
            其實(shí),引入反碼,我么已經(jīng)可以將減法統(tǒng)一變作加法【?1-1=1+(-1)】進(jìn)行正確的計(jì)算了,已經(jīng)解決了符號位的問題了,但會產(chǎn)生 -0 和 +0
          的問題,也就是 0 被帶上了符號。雖然在人腦看來是正負(fù) 0 一樣的,但是計(jì)算機(jī)可不那么認(rèn)為,而且按照定義 0 會有兩種原碼表示,即 000 0000 和
          1000 0000,這顯然是有問題的。于是在反碼的基礎(chǔ)之上加 1 變補(bǔ)碼,徹底解決了正負(fù) 0 的問題,以前表示 -0 的1000 0000 現(xiàn)在可以用來表示
          -128,因?yàn)?-128 = -1-127=(-1)+(-127)=(1111 1111)補(bǔ)+(1000 0001)補(bǔ)=1000
          0000?!@也是帶符號位二進(jìn)制能夠多表示一個數(shù)的原因。下面是博主探究二進(jìn)制運(yùn)算的過程中畫的原碼和補(bǔ)碼計(jì)算的結(jié)果差異圖:

              

          ?

            上圖至少說明了兩點(diǎn):

              第一,帶符號二進(jìn)制直接用原碼進(jìn)行加減運(yùn)算特別不靠譜,而通過補(bǔ)碼進(jìn)行加法(減也看作加)運(yùn)算很靠譜;

              第二,如果運(yùn)算結(jié)果是正數(shù),由于正數(shù)的原碼和補(bǔ)碼相同,所以結(jié)果和十進(jìn)制數(shù)是正確匹配的,如果結(jié)果是負(fù)數(shù),需要將補(bǔ)碼轉(zhuǎn)成原碼方能匹配正確的十進(jìn)制結(jié)果;

            雖然補(bǔ)碼解決了問題,但是博主還是有疑問——難道計(jì)算機(jī)科學(xué)家是先知,他們怎么知道將原碼求反碼后再加 1
          得到的補(bǔ)碼就能夠解決符號位的運(yùn)算問題?感覺像是碰巧一樣,毫無道理嘛!但正確無比的結(jié)果又似乎在告訴我,補(bǔ)碼的產(chǎn)生背后,肯定有某種隱含的邏輯。。。(思考ing)。。。補(bǔ)碼補(bǔ)碼,為什么叫補(bǔ)碼,沒學(xué)過計(jì)算機(jī)的我只聽過補(bǔ)數(shù)?。窟?,會不會和補(bǔ)數(shù)有關(guān)系呢,不然為什么都姓
          補(bǔ) 呢?一番琢磨,臥槽,還真的有關(guān)系。在十進(jìn)制中,如果兩個數(shù)相加能湊成十或成百的整數(shù),我們就可以把其中一個數(shù)叫另一個數(shù)的補(bǔ)數(shù),因此可以說 4 和 6
          互為補(bǔ)數(shù);同樣的對于二進(jìn)制來說,我們也是可以湊個整數(shù)的,湊整就有補(bǔ)數(shù),而補(bǔ)數(shù)對于運(yùn)算往往大有幫助!這里拿一個 4
          位二進(jìn)制來說,若不考慮符號位,其能表示的最大數(shù)為 1111,包含 0 在內(nèi)總共能表示 16 個數(shù),那么這個 16 就是一個?整數(shù)。如果要計(jì)算
          7-3,我們可以嘗試帶入補(bǔ)數(shù)的思想,先用 7 加 3 的補(bǔ)數(shù) 13 看會有什么發(fā)現(xiàn)。這是很簡單的算術(shù)問題, 7+13 等于 20,和 7-3 的結(jié)果 4
          差的有點(diǎn)遠(yuǎn)。但是,差的是什么呢?恰好就是我們前面提到的那個整數(shù)啊,如果我們用 20 減掉整數(shù) 16,恰恰就是我們要的結(jié)果 4 !巧合嗎?這可不是巧合,這是因?yàn)?
          20 已經(jīng)超出了4 位二進(jìn)制所能表示的最大數(shù),產(chǎn)生進(jìn)位溢出,這個溢出的數(shù)剛好就是那個整數(shù) 16。換成二進(jìn)制表示你一定就了然了:
              7-3 = 0111-0011=0111+1101=0100(10100進(jìn)位溢出舍棄高位的1)
            如果你還是不太理解,那么,博主將上面表述中特意強(qiáng)調(diào)的整數(shù)換成計(jì)算機(jī)術(shù)語中更常用的 模 你應(yīng)該就恍然有感了。為什么上述中的整數(shù)也可以表述成模
          呢?因?yàn)槎M(jìn)制的進(jìn)位溢出其實(shí)同模運(yùn)算中遇整舍棄只留余數(shù)是一樣的道理。具體到生活中我們可以用時鐘來作比。時針在表盤上走一圈是12個點(diǎn),因此 12
          這個整就是一個模長,如果現(xiàn)在時針停在 12 點(diǎn)處,要讓它指向 10 點(diǎn),可以怎么做?順時針(+)走10個點(diǎn)也行,逆時針(-)回 2 個點(diǎn)也行,而恰好 10 和
          2 之間是互補(bǔ)的,于是,根據(jù)模長和補(bǔ)數(shù)的關(guān)系,我們就成功的將減法轉(zhuǎn)換成了加法運(yùn)算,這就是為什么上面的 7-3 可以換成
          7+13的原因,由此可見,在帶符號二進(jìn)制的算數(shù)運(yùn)算中,引入補(bǔ)碼,其意思很明確,就是為了統(tǒng)一運(yùn)算符。

            回過頭來可以解答開頭的問題——為什么科學(xué)家先知一樣的就知道負(fù)數(shù)的補(bǔ)碼是其反碼加 1 呢? 根據(jù)博主對補(bǔ)數(shù)和模的粗淺解釋,我們可以自己來算下。帶符號的 4
          位 二進(jìn)制能表示的最大數(shù)是 7,最小數(shù)是 -8,模長依然是 16。在這個單字節(jié)范圍內(nèi)的負(fù)數(shù),比如 -3 ,二進(jìn)制表示為 1011
          。以上面博主說過減法變加法的方式,取 3 的補(bǔ)數(shù) 13,二進(jìn)制表示為 1101,這不正是 -3 的補(bǔ)數(shù)嘛!所以,負(fù)數(shù)的補(bǔ)碼真不是科學(xué)家先知一般知道就是反碼加
          1,只不過運(yùn)算出來恰好就是反碼加 1 ,這也是算出補(bǔ)碼最簡單的方法了,于是也就那樣去表述,并不是理論基礎(chǔ)。

          位運(yùn)算


            二進(jìn)制的運(yùn)算其實(shí)還不止于上面看到的基本的算數(shù)運(yùn)算,還有一種運(yùn)算叫邏輯運(yùn)算——直接操作二進(jìn)制中的位,而不涉及算術(shù)運(yùn)算中的進(jìn)位和借位,所以也叫位運(yùn)算。面試你可能遇到過諸如
          "寫出 2*8
          最有效率的運(yùn)算方法"之類的問題,無非就是考你對于底層二進(jìn)制的熟悉程度。不用說,當(dāng)然是用位運(yùn)算效率最高咯。所以,掌握一點(diǎn)位運(yùn)算,在一些問題解決上,常常會有一些巧技。博主簡述一下常見的邏輯運(yùn)算,為最后的闡述做鋪墊。

            按位與(&)

            相對應(yīng)的二進(jìn)制位同為 1 結(jié)果才為 1,否則都是 0,形如:0&0=0,0&1=0,1&0=0,1&1=1 。?
          利用這個特性,我們判斷奇偶數(shù)就可以不用再傳統(tǒng)的 n%2的方式了,直接用 n&1,結(jié)果為 1 就是奇數(shù),為 0 就是偶數(shù)。why? 因?yàn)?
          0或正數(shù),補(bǔ)碼和原碼相同,由于 1 的前 n 位都是 0 ,與 1 相與,結(jié)果肯定是 0 ,我們只關(guān)心最后一位,奇數(shù)肯定是
          1,1與1相與結(jié)果為1;若為負(fù)數(shù),原碼轉(zhuǎn)反碼時,奇數(shù)最后一位由 1 變 0,但轉(zhuǎn)補(bǔ)碼后有加 1 操作,末尾為 1 ,判定同理。

            按位或(|)

            相對應(yīng)的二進(jìn)制位只要有一個為 1 ,結(jié)果即為 1,形如:0|0=0,0|1=1,1|0=1,1|1=1。?

            按位異或(^)

            相對應(yīng)的二進(jìn)制位數(shù)字不同,結(jié)果為 1 ,否則都是 0 ,形如:0^0=0,0^1=1,1^0=1,1^1=0。異或有個特性就是,任何數(shù)與 0
          異或,結(jié)果都是其本身。利用這個特性,可用于數(shù)的交換,以此可以解決一些面試刁難:如何在不采用臨時變量的情況下實(shí)現(xiàn)兩個數(shù)的交換?當(dāng)然,不用位運(yùn)算也是可以實(shí)現(xiàn)的,只是不那么高級。常見寫法奉上:
          int a = 2; int b = 3; //方式一 a=a+b; b=a-b; a=a-b; // 方式二 a=a^b; b=a^b; a=a^b;
            取反(~)

            二進(jìn)制位按位取反,0 變 1 ,1 變 0 。

            左移(<<)

            形如 a<<b,將 a 的各二進(jìn)制位整體向左移 b 位,高位溢出位移出,低位補(bǔ) 0。在數(shù)值沒有溢出的情況下,左移n位相當(dāng)于乘 2 的n次方。例如
          2<<3,即由二進(jìn)制的 00000010 變成了 0010000,相當(dāng)于 2 乘 2 的 3 次方,結(jié)果為 16。因?yàn)槲贿\(yùn)算是 CPU
          直接支持的,這也就是上面提到的 2*8 最有效率的運(yùn)算方法了。

            右移(>>)

            形如 a>>b ,原理同左移,只不過由于符號位在最高位,所以,如果右移的是負(fù)數(shù),會在高位補(bǔ) 1 ,如果為正數(shù),高位補(bǔ) 0 。

            無符號右移(>>>)

            與右移唯一的不同在于,不論原來最左邊是什么數(shù),移動后都在高位補(bǔ) 0。注意,沒有無符號左移, 因?yàn)樽笠剖冀K是在右邊補(bǔ) 0
          ,而符號位在左邊,不存在補(bǔ)符號位的問題。

            

          終章 


            嗶嗶了這么多,還是回到開始吧??戳瞬┲魃厦鏌o頭無腦的分析,相信你早已明白,長字節(jié)的數(shù)要往短了轉(zhuǎn),直接強(qiáng)來,肯定是不行的。那就不轉(zhuǎn)唄,反正也很少遇到。NO,NO,NO!只需要翻看一下Java的IO包中的各種輸入輸出流的讀寫方法,就可以發(fā)現(xiàn),很多參數(shù)都是字節(jié)數(shù)組,因?yàn)樽止?jié)可以說是計(jì)算機(jī)中能表示信息含義的最小單位了,尤其在網(wǎng)絡(luò)編程中,為了不同通訊終端的數(shù)據(jù)兼容,發(fā)送和接受的數(shù)據(jù)基本都是字節(jié)序列,所以,知道如何將長字節(jié)數(shù)變短,也是很有必要滴。

            那么,在Java中,我們怎么將一個int類型,轉(zhuǎn)換成byte 還能成功的還原呢?直接強(qiáng)轉(zhuǎn),超過范圍的部分,肯定是裝不下的,不過我們知道,一個 int
          占用 4個 byte,換句話說,我們可以用一個長度為 4 的 byte數(shù)組來裝:




            看圖就知道,裝進(jìn)byte數(shù)組是容易的(這里的裝法也可以反序來,即byte[0]裝低8位,以此類推,還原相應(yīng)調(diào)整順序,只要明白原理,都OK),主要的問題在于如何將
          int 拆分成單個字節(jié)放進(jìn)數(shù)組。看圖就知道,其實(shí)也比較簡單,就是進(jìn)行位運(yùn)算中的右移(>>)操作,不然博主上面位運(yùn)算鋪墊個鏟鏟啊,不多說,代碼一目了然:
          public static byte[] int2Bytes(int i) { byte[] bytes = new byte[4]; bytes[0] =
          (byte) (i >> 24); bytes[1] = (byte) (i >> 16); bytes[2] = (byte) (i >> 8);
          bytes[3] = (byte) i; return bytes; }
            裝是裝進(jìn)去了,怎么還原呢?我們剛剛是進(jìn)行了進(jìn)行了右移操作,要還原的話,很自然的我們想到要左移(<<),稍微有點(diǎn)位運(yùn)算基礎(chǔ),似乎實(shí)現(xiàn)起來也簡單:
          public static int bytes2Int(byte[] bytes) { // 左移將原來的數(shù)先還原到對應(yīng)的位置,再 按位或 將幾個數(shù)進(jìn)行合并
          return   bytes[3] | bytes[2] << 8 | bytes[1] << 16 | bytes[0] << 24; }
            如果你真這樣搞,那就等著大大的bug吧!如上圖博主畫圖是 256 的二進(jìn)制存儲位序列圖,用上面的方法還原出來倒是沒問題,你多換幾個數(shù)試試,比如-258,
          245677,-2677等等?這都是博主隨便舉的數(shù)字,沒有啥特殊,但結(jié)果會讓你大跌眼鏡。為什么通過右移裝進(jìn)數(shù)組再按照同樣的思維方式左移還原就不行了呢?那是因?yàn)?br>計(jì)算機(jī)對二進(jìn)制的運(yùn)算和存儲都是以補(bǔ)碼方式來進(jìn)行的啊,親。-258 在 int 中存的樣子不是你以為的這個樣子:



          ?  而應(yīng)該是下面這個樣子:



          ?

          ?  因此,我們裝到字節(jié)數(shù)組中的就是第二份草圖中存儲位序列中的每一個字節(jié)段。而當(dāng)我們用左移想進(jìn)行還原的時候,byte
          數(shù)組中每個byte左移后的結(jié)果其實(shí)是下面這樣的:



          ?

          ?

            對于上面的草圖,博主解釋一下。在進(jìn)行移位運(yùn)算時,byte,short,char 的類型會提升為 4 字節(jié) 32 位的 int 型,就需要用 0 或 1
          進(jìn)行補(bǔ)位,如果是負(fù)數(shù),會在前面補(bǔ) 1 ,如果是正數(shù)則補(bǔ) 0 。從上圖左移補(bǔ)齊后的存儲序列來看,如果此時將得到的 4 個 int 值進(jìn)行按位或(|)
          操作(未移位的byte[3]此時也會補(bǔ)位到 32 位),結(jié)果就是下面這樣:



          ?  哇,這個結(jié)果,看起來這個數(shù)好大的樣子,其實(shí)不大,因?yàn)椴┲髟缇驼f過了計(jì)算機(jī)是以補(bǔ)碼的形式存儲二進(jìn)制的,將該補(bǔ)碼轉(zhuǎn)回原碼你會發(fā)現(xiàn),才等于 -2
          ,挺小的,不過和原來的 -258相差太多了。分析下來,其實(shí)你已經(jīng)發(fā)現(xiàn)了,還是因?yàn)榉栁辉趽v亂:當(dāng)還原前字節(jié)數(shù)組中有負(fù)數(shù)的時候,在提升為 int 補(bǔ)位的時候補(bǔ)1
          就補(bǔ)出了問題。如何解決呢,很簡單,將負(fù)數(shù)本來補(bǔ)的 1 置為 0 。通常的做法是采取將字節(jié)數(shù)先和 0xff(00000000 00000000 00000000
          11111111)進(jìn)行?按位與(&)操作,在電計(jì)算機(jī)補(bǔ) 1 之前,我們自己先給補(bǔ) 0 到32位,形如布衣草圖:



            所以,上面還原 int 的方法該這樣寫:
          public static int bytes2Int(byte[] bytes) { return   bytes[3] & 0xff |
          (bytes[2] & 0xff) << 8 | (bytes[1] & 0xff) << 16 | (bytes[0] & 0xff) << 24; }
          ?  好了,結(jié)尾勿煽情,嗶完收工!若博主闡述有不恰當(dāng)?shù)牡胤?,歡迎留言指正;推不推薦的,隨緣隨便隨你吧。

          ?

          友情鏈接
          ioDraw流程圖
          API參考文檔
          OK工具箱
          云服務(wù)器優(yōu)惠
          阿里云優(yōu)惠券
          騰訊云優(yōu)惠券
          京東云優(yōu)惠券
          站點(diǎn)信息
          問題反饋
          郵箱:[email protected]
          QQ群:637538335
          關(guān)注微信

                人人妻人人澡人人爽人人到DVD | 制服诱惑一区二区三区 | 亚洲男人网站 | 1级大片 爆乳极品自慰91久久久久 | 国产精品九九九九九 |