最近在使用 Java 作為 WebSocket 客戶(hù)端連接 Node.js 的 WebSocket 服務(wù)器的時(shí)候,由于使用的客戶(hù)端庫比較老,所以遇到了字節符號的問(wèn)題,上網(wǎng)查了一下,看到這篇文章寫(xiě)的很有意思,就翻譯一下。
原文地址: http://www.darksleep.com/player/JavaAndUnsignedTypes.html
以下是正文
在 C 和 C++ 這樣的語(yǔ)言中,都提供了不同長(cháng)度的整數類(lèi)型: char, short, int, long(實(shí)際上, char并不是真正的整數,但是你可以把它當成整數來(lái)用。在實(shí)際應用場(chǎng)景中,很多人在 C 語(yǔ)言中用 char來(lái)存儲較小的整數)。在大部分的 32 位操作系統上,這些類(lèi)型分別對應 1 字節,2 字節,4 字節和 8 字節。但是需要注意的是,這些整數類(lèi)型所對應的字節長(cháng)度在不同的平臺上是不一樣的。相對而言,由于 Java 是針對跨平臺來(lái)設計的,所以無(wú)論運行在什么平臺上,Java 中的 byte永遠是 1 字節, short是 2 字節, int是 4 字節, long是 8 字節。
C 語(yǔ)言中的整數類(lèi)型都提供了對應的“無(wú)符號”版本,但是 Java 中就沒(méi)有這個(gè)特性了。我覺(jué)得 Java 不支持無(wú)符號類(lèi)型這個(gè)事兒實(shí)在是太不爽了,你想想,大量的硬件接口、網(wǎng)絡(luò )協(xié)議以及文件格式都會(huì )用到無(wú)符號類(lèi)型?。↗ava 中提供的 char類(lèi)型和 C 中的 char有所不同,在 Java 中, chat是用 2 個(gè)字節來(lái)表示 Unicode 值,在 C 中, char是用 1 個(gè)字節來(lái)表示 ASCII 值。雖然可以在 Java 中把 char當做無(wú)符號短整型來(lái)使用,用來(lái)表示 0 到 2^16 的整數。但是這樣來(lái)用可能產(chǎn)生各種詭異的事情,比如當你要打印這個(gè)數值的時(shí)候實(shí)際上打印出來(lái)的是這個(gè)數值對應的字符而不是這個(gè)數值本身的字符串表示)。
好吧,對于我給出的這種方案,你可能會(huì )不喜歡……
答案就是:使用比要用的無(wú)符號類(lèi)型更大的有符號類(lèi)型。
例如:使用 short來(lái)處理無(wú)符號的字節,使用 long來(lái)處理無(wú)符號整數等(甚至可以使用 char來(lái)處理無(wú)符號短整型)。確實(shí),這樣看起來(lái)很浪費,因為你使用了 2 倍的存儲空間,但是也沒(méi)有更好的辦法了。另外,需要提醒的是,對于 long類(lèi)型變量的訪(fǎng)問(wèn)不是原子性操作,所以,如果在多線(xiàn)程場(chǎng)景中,你得自己去處理同步的問(wèn)題。
如果有人從網(wǎng)絡(luò )上給你發(fā)送了一堆包含無(wú)符號數值的字節(或者從文件中讀取的字節),那么你需要進(jìn)行一些額外的處理才能把他們轉換到 Java 中的更大的數值類(lèi)型。
還有一個(gè)就是字節序問(wèn)題。但是現在我們先不管它,就當它是“網(wǎng)絡(luò )字節序”,也就是“高位優(yōu)先”,這也是 Java 中的標準字節序。
假設我們開(kāi)始處理一個(gè)字節數組,我們希望從中讀取一個(gè)無(wú)符號的字節,一個(gè)無(wú)符號短整型和一個(gè)無(wú)符號整數。
short anUnsignedByte = 0; char anUnsignedShort = 0; long anUnsignedInt = 0; int firstByte = 0; int secondByte = 0; int thirdByte = 0; int fourthByte = 0; byte buf[] = getMeSomeData(); // Check to make sure we have enough bytes if(buf.length < (1 + 2 + 4)) doSomeErrorHandling(); int index = 0; firstByte = (0x000000FF & ((int)buf[index])); index++; anUnsignedByte = (short)firstByte; firstByte = (0x000000FF & ((int)buf[index])); secondByte = (0x000000FF & ((int)buf[index+1])); index = index+2; anUnsignedShort = (char) (firstByte << 8 | secondByte); firstByte = (0x000000FF & ((int)buf[index])); secondByte = (0x000000FF & ((int)buf[index+1])); thirdByte = (0x000000FF & ((int)buf[index+2])); fourthByte = (0x000000FF & ((int)buf[index+3])); index = index+4; anUnsignedInt = ((long) (firstByte << 24 | secondByte << 16 | thirdByte << 8 | fourthByte)) & 0xFFFFFFFFL; 好吧,現在看起來(lái)有一點(diǎn)兒復雜。但是實(shí)際上很直觀(guān)。首先,你看到很多這樣的東東:
0x000000FF & (int)buf[index] 首先,把有符號的 byte提升成 int類(lèi)型,然后對這個(gè) int進(jìn)行按位與操作,僅保留最后 8 個(gè)比特位。因為 Java 中的 byte是有符號的,所以當一個(gè) byte的無(wú)符號值大于 127 的時(shí)候,表示符號的二進(jìn)制位將被設置為 1(嚴格來(lái)說(shuō),這個(gè)不能算是符號位,因為在計算機中數字是按照補碼方式編碼的),對于 Java 來(lái)說(shuō),這個(gè)就是負數。當將負數數值對應的 byte提升為 int類(lèi)型的時(shí)候,0 到 7 比特位將會(huì )被保留,8 到 31 比特位會(huì )被設置為 1。然后將其與 0x000000FF進(jìn)行按位與操作來(lái)擦除 8 到 31 比特位的 1。上面這句代碼可以簡(jiǎn)短的寫(xiě)作:
0xFF & (int)buf[index] Java 自動(dòng)填充 0xFF的前導的 0 ,并且在 Java 中,位操作符 &會(huì )導致 byte自動(dòng)提升為 int。
接下來(lái)你看到的是很多的按位左移運算符 <<。 這個(gè)操作符會(huì )對左操作數按位左移右操作數指定的比特位。所以,如果你有一個(gè) int foo = 0x000000FF,那么 foo << 8會(huì )得到 0x0000FF00, foo << 16會(huì )得到 0x00FF0000。
最后是按位或操作符 |。假設你現在把一個(gè)無(wú)符號短整型的 2 個(gè)字節加載到了對應的整數中,你會(huì )得到 0x00000012和 0x00000034兩個(gè)整數?,F在你把第一個(gè)字節左移 8 位得到 0x00001200和 0x00000034,然后你需要把他們再拼合回去。所以需要進(jìn)行按位或操作。 0x00001200 | 0x00000034會(huì )得到 0x00001234,這樣就可以存儲到 Java 中的 char類(lèi)型。
這些都是基礎操作。但是對于無(wú)符號 int,你需要把它存儲到 long類(lèi)型中。其他操作和前面類(lèi)似,只是你需要把 int提升為 long然后和 0xFFFFFFFFL進(jìn)行按位與操作。最后的 L用來(lái)告訴 Java 請把這個(gè)常量視為 long來(lái)處理。
假設現在我們要把上面步驟中我們讀取到的數值寫(xiě)入到緩沖區。我們當時(shí)是按照無(wú)符號 byte,無(wú)符號 short和無(wú)符號 int的順序讀取的,現在,甭管什么原因吧,我們打算按照無(wú)符號 int,無(wú)符號 short和無(wú)符號 byte的順序來(lái)寫(xiě)出。
buf[0] = (anUnsignedInt & 0xFF000000L) >> 24; buf[1] = (anUnsignedInt & 0x00FF0000L) >> 16; buf[2] = (anUnsignedInt & 0x0000FF00L) >> 8; buf[3] = (anUnsignedInt & 0x000000FFL); buf[4] = (anUnsignedShort & 0xFF00) >> 8; buf[5] = (anUnsignedShort & 0x00FF); buf[6] = (anUnsignedByte & 0xFF); Java 中所使用的“高位優(yōu)先”字節序又被稱(chēng)為“網(wǎng)絡(luò )字節序”。Intel x86 處理器是“低位優(yōu)先”字節序(除非你在上面運行 Java 程序)。x86 系統創(chuàng )建的數據文件通常是(但不是必須的)低位優(yōu)先的,而 Java 程序創(chuàng )建的數據文件通常是(但不是必須的)高位優(yōu)先的。任何系統都可以按照自己需要的字節序來(lái)輸出數據。
“字節序”是指計算機是按照何種順序在內存中存儲數值的。常見(jiàn)的無(wú)非是高位優(yōu)先和低位優(yōu)先兩種模式。你當然需要關(guān)注字節序的問(wèn)題了,否則,如果你按照高位優(yōu)先的字節序去讀取一個(gè)低位優(yōu)先字節序存儲的數據文件,很可能就只能得到亂七八糟的數據了,反之亦然。
任何數值,無(wú)論是何種表達方式,比如 5000,000,007或者它的 16 進(jìn)制格式 0x1DCD6507,都可以看做是數字字符串。對于一個(gè)數字字符串,我們可以認為它有開(kāi)始(最左),有結束(最右)。在英語(yǔ)中,第一個(gè)數字就是最高位數字,例如 5000,000,007中的 實(shí)際上表示的是 500,000,000。最后一位數字是最低位數字,例如 500,000,007中的 對應的值是 。
當我們說(shuō)到字節序的時(shí)候,我們是參照我們寫(xiě)數字時(shí)候的順序。我們總是從高位開(kāi)始寫(xiě),然后是次高位,直到最低位,是不是這樣???
在上面的例子中,數值 500,000,007,對應 16 進(jìn)制表示方式是 0x1DCD6507,我們把它分成 4 個(gè)獨立的字節: 0x1D, 0xDC, 0x65和 0x07,對應 10 進(jìn)制的值 29, 205, 101 和 7。最高位字節 29 表示 29 *256 * 256 * 256 = 486539264,接下來(lái)是 205,表示 205 * 256 * 256 = 13434880,然后是 101,表示 101 * 256 = 25856,最后一個(gè) 7 就是 7 * 1 = 7。它們的值:
486539264 + 13434880 + 25856 + 7 = 500,000,007
當計算機在它的內存中存儲這 4 個(gè)字節的時(shí)候,假設存儲到內存的地址是 2056, 2057, 2058 和 2059。那么問(wèn)題來(lái)了:到底在哪個(gè)內存地址上存儲哪個(gè)字節呢?它可能是在地址 2056 存儲 29, 2057 存儲 205,2058 存儲 101,2059 存儲 7,就像你寫(xiě)下這個(gè)數字的順序一樣,我們稱(chēng)之為高位優(yōu)先。但是,其他的計算機架構可能是在 2056 存儲 7,2057 存儲 101, 2058 存儲 205, 2059 存儲 29,這樣的順序我們稱(chēng)之為低位優(yōu)先。
針對 2 個(gè)字節的以及 8 個(gè)字節的存儲方式,也是同樣的。最高位字節稱(chēng)為 MSB,最低位字節稱(chēng)為 LSB。
這個(gè)視情況而定了。通常情況下你不需要關(guān)心這個(gè)問(wèn)題。無(wú)論你在什么平臺運行 Java 程序,它的字節序都是一樣的,所以你就無(wú)需關(guān)心字節序的問(wèn)題。
但是,當你要處理其他語(yǔ)言產(chǎn)生的數據呢?那么,字節序就是一個(gè)大問(wèn)題了。你必須得保證你按照數據被編碼的順序來(lái)進(jìn)行解碼,反之亦然。如果你足夠幸運,通常在 API 或者協(xié)議規范、文件格式說(shuō)明中找到關(guān)于字節序的說(shuō)明。如果不巧……祝你好運吧!
最重要的是,你需要清晰的了解你所使用的字節序是什么樣的以及你需要處理的數據的字節序是什么樣的。如果二者不同,你需要進(jìn)行額外的處理來(lái)保證正確性。還有就是,如果你需要處理無(wú)符號數值,你需要確保將正確的字節放到對應 integer/short/long類(lèi)型的正確位置。
當設計 IP 協(xié)議的時(shí)候,高位優(yōu)先字節序被設計為網(wǎng)絡(luò )字節序。在 IP 報文中德數值類(lèi)型都是按照網(wǎng)絡(luò )字節序存儲的。產(chǎn)生報文的計算機所使用的字節序稱(chēng)為“宿主機字節序”,可能和網(wǎng)絡(luò )字節序一樣,也可能不一樣。和網(wǎng)絡(luò )字節序一樣,Java 中的字節序是高位優(yōu)先的。
為什么 Java 不提供無(wú)符號類(lèi)型呢?好問(wèn)題!我也常常覺(jué)得這個(gè)事情非常詭異,尤其是當時(shí)已經(jīng)有很多網(wǎng)絡(luò )協(xié)議都使用無(wú)符號類(lèi)型了。在 1999 年,我在 Web 上也找了很久(那個(gè)時(shí)候 google 還沒(méi)有這么棒),因為我總是覺(jué)得這事兒不應該是這樣。直到有一天我采訪(fǎng) Java 發(fā)明者中的一位(是 Gosling 嗎?不太記得了,要是我保存了當時(shí)的網(wǎng)頁(yè)就好了),這位設計者說(shuō)了一段話(huà),大意是:“嘿!無(wú)符號類(lèi)型把事情搞復雜了,沒(méi)有人真正需要無(wú)符號類(lèi)型,所以我們把它趕出去了”。
這里有一個(gè)頁(yè)面,是記錄了一次對 James Gosling 的采訪(fǎng),看看能否收到一些啟發(fā):
http://www.gotw.ca/publications/c_family_interview.htm
問(wèn):程序員經(jīng)常討論使用“簡(jiǎn)單語(yǔ)言”編程的優(yōu)點(diǎn)和缺點(diǎn)。你怎么看待這個(gè)問(wèn)題?你覺(jué)得 C/C++/Java 算是簡(jiǎn)單語(yǔ)言嗎?
Ritchie: 略
Stroustrup:略
Gosling:作為一個(gè)語(yǔ)言設計者,我不太理解所謂的“簡(jiǎn)單”結束了是什么意思,我希望 Java 開(kāi)發(fā)者把這個(gè)概念留在他自己腦海里就好啦。舉例來(lái)說(shuō),按照那個(gè)定義,Java 不算是簡(jiǎn)單語(yǔ)言。實(shí)際上很多語(yǔ)言都會(huì )在極端案例下完蛋,那些極端案例是人們都不會(huì )理解的。你去問(wèn) C 語(yǔ)言開(kāi)發(fā)人員關(guān)于無(wú)符號的問(wèn)題,你很快就會(huì )發(fā)現沒(méi)有幾個(gè) C 語(yǔ)言開(kāi)發(fā)人員真正理解無(wú)符號類(lèi)型到底發(fā)生了些什么,什么是無(wú)符號運算。這些事情讓 C 語(yǔ)言變得復雜。我覺(jué)得 Java 語(yǔ)言是非常簡(jiǎn)單的。
另外,參考:
http://www.artima.com/weblogs/viewpost.jsp?thread=7555
Oak 往事……
by Heinz Kabutz
2003 年 7 月 15
為了豐富我對 Java 歷史的了解,我開(kāi)始研究 Sun 的網(wǎng)站,無(wú)意間發(fā)現了 Oak 0.2 的語(yǔ)言規范書(shū)。Oak 是 Java 語(yǔ)言最早使用的名稱(chēng),這份文檔算是現存的最古老的關(guān)于 Oak 的文檔了。
……
無(wú)符號整數(3.1 節)
規范書(shū)說(shuō):“8 比特,16 比特,32 比特,64 比特的,這 4 種不同寬度的整數類(lèi)型都是有符號的,除非在前面加上 unsigned修飾符”。
在側欄中又說(shuō):“無(wú)符號類(lèi)型尚未實(shí)現;可能永遠也不會(huì )實(shí)現了?!?好吧,就是這樣了。
聯(lián)系客服