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