位元組的遊戲
業務處理上,有時會直接對位元組進行操作。例如實現私有協議,對校驗位進行檢測,敏感資料加密等。博主查了 一下網上的資料,發現有不少都是錯誤的。甚至連《Thinking in Java》的解釋都很令人困惑,以下是從書中摘錄的原文:
如果對char、byte或者short型別的數值驚醒移位處理,那麼在移位之前,他們會被轉換為int型別,並且得到的結果也是一個int型別。只有數值右端的低5位才有用。
當時讀到這一句的時候,我理解了很久,至今沒有明白“只有數值右端的低5位才有用”的含義。理解位元組處理的基本方法就是動手操作,下面我會結合用例進行解釋。
首先,我們需要理解幾個基礎概念。一般來說,位元組是我們可以用語言處理的最小物件,無論是C/C++還是Java都沒有直接提供bit型別。1 byte = 8 bit,除去最左側的符號位1byte可以描述的範圍是:-128 ~ 127。但是在大多數的業務處理中,我們通常會採用無符號位,即用1byte表示:0 ~ 255。其次,常見的移位操作符有左移(<<) 和右移 (>>),比較容易忽視的是右移操作,如果最左側的符號位為1則右移是在高位插入的是——1。因此Java中增加了一種“無符號”右位移操作符>>>,通常用不上了解即可。最後,如果我們採用byte[]來表示一種資料型別,陣列下標從小到大即記憶體地址的從低位到高位。記住這個概念非常重要,後面我會引入大端模式與小端模式。
為了讓大家理解以上概念,下面看兩個例子:
1. 假設byte x = 127,對它執行左移1位的操作 x = ?
byte x = 127; x <<= 1; System.out.println(Integer.toHexString(x));
在程式碼執行之前我們先使用計算器計算一下:BIN(1111 1110) HEX(FE),程式碼的執行結果為:FFFFFFFE。原因是對x左移1位超出了byte的表示範圍,Java自動在左側補位,由於最高位是1,因此我們獲得了一個怪異的結果。那麼有什麼辦法得到一個正確的結果呢?
byte x = 127; x <<= 1; System.out.println(Integer.toHexString(x & 0xFF));
2. 假設byte x = 1,對它執行左移32位的操作 x = ?
byte x = 1; System.out.println(x << 32);
答案是1。這個結論比較怪異而且確實是一個坑,大家只需要記住:對一個int值來說,左移32位等於它的原始值;對於一個long值來說,左移64位等於它的原始值。
在理解了這些基本概念以後,我們已經做好了進入位元組世界的準備。
我們如何用4個位元組的大端模式表示一個整型變數?
對大端模式的定義為:資料的高位元組儲存在記憶體的低地址中,而資料的低位元組儲存在記憶體的高地址中。這個說法很繞而且也不利於理解,對於數字常量來說0x1234,1即為高位,4即為低位。而對於byte[4]來說,bs[0]即為地址低位,bs[3]即為地址高位。這樣看來就很清楚了。大端模式符合人們的閱讀模式。
int i = 0x1234; byte[] bs = new byte[4]; bs[3] = (byte) (i & 0xFF); bs[2] = (byte) (i >> 8 & 0xFF); bs[1] = (byte) (i >> 16 & 0xFF); bs[0] = (byte) (i >> 24 & 0xFF); for(byte b : bs) { System.out.println(Integer.toHexString(b)); }
更抽象的演算法,大家可以在理解了上面的例子以後自己封裝。
反過來我們將以大端模式生成的4個位元組還原為一個整型數?
int x = bs[3] & 0xFF; x |= bs[2] & 0xFF << 8; x |= bs[1] & 0xFF << 16; x |= bs[0] & 0xFF << 24; System.out.println(Integer.toHexString(x));
注意:為了得到正確的結果,我們在對byte進行移位前一定要先做位與(&)操作。
接下來我們需要升級問題,將一個8個位元組寬度的符合大端模式的位元組陣列還原為一個長整型數。
long x = bs[7] & 0xFF; x |= (bs[6] & 0xFF) << 8; x |= (bs[5] & 0xFF) << 16; x |= (bs[4] & 0xFF) << 24; x |= (bs[3] & 0xFF) << 32; x |= (bs[2] & 0xFF) << 40; x |= (bs[1] & 0xFF) << 48; x |= (bs[0] & 0xFF) << 56; System.out.println(Long.toHexString(x));
似乎我們很容易按照整型的轉換方式得到以上演算法。不幸的是,這樣做是錯誤的。如果這個byte[]表示的數字範圍超過整型數的上限,我們將無法獲得正確的長整型數。原因是Java預設在對byte進行移位操作前會轉換為int型別,還記得上面我們讓大家記住“對一個int值來說,左移32位等於它的原始值”嗎?正確的做法應該是這樣:
long x = bs[7] & 0xFF; x |= ((long)bs[6] & 0xFF) << 8; x |= ((long)bs[5] & 0xFF) << 16; x |= ((long)bs[4] & 0xFF) << 24; x |= ((long)bs[3] & 0xFF) << 32; x |= ((long)bs[2] & 0xFF) << 40; x |= ((long)bs[1] & 0xFF) << 48; x |= ((long)bs[0] & 0xFF) << 56; System.out.println(Long.toHexString(x));
至此我們應該可以很輕鬆的解決有關位元組轉換的各種難題了,但是上面的這些演算法未免顯得太不優美,幸虧Java早就為我們想到了這一點。本著不要重複造輪子的觀點,我提供了一套工具。
/** * 任意位元組寬度轉換為標準整型數 */ public static int bytesToInt(byte[] bytes, int byteNum, ByteOrder order) { ByteBuffer buffer = ByteBuffer.allocate(4); buffer.order(order); buffer.put(bytes, 0, bytes.length); buffer.put(new byte[buffer.limit() - byteNum], 0, buffer.limit() - byteNum); buffer.flip(); return buffer.getInt(); } /** * 長整型數轉換為指定位元組寬度 */ public static byte[] longToBytes(long x, int byteNum, ByteOrder order) { ByteBuffer buffer = ByteBuffer.allocate(8); buffer.order(order); buffer.putLong(0, x); return Arrays.copyOfRange(buffer.array(), 0, byteNum); } /** * 任意位元組寬度轉換為長整型 */ public static long bytesToLong(byte[] bytes, int byteNum, ByteOrder order) { ByteBuffer buffer = ByteBuffer.allocate(8); buffer.order(order); buffer.put(bytes, 0, bytes.length); buffer.put(new byte[buffer.limit() - byteNum], 0, buffer.limit() - byteNum); buffer.flip(); return buffer.getLong(); } /** * 長整型數轉換為標準的8位元組寬度 */ public static byte[] longToBytes(long x, ByteOrder order) { ByteBuffer buffer = ByteBuffer.allocate(8); buffer.order(order); buffer.putLong(0, x); return buffer.array(); } /** * 標準8位元組寬度轉換為長整型數 */ public static long bytesToLong(byte[] bytes, ByteOrder order) { ByteBuffer buffer = ByteBuffer.allocate(8); buffer.order(order); buffer.put(bytes, 0, bytes.length); buffer.flip(); return buffer.getLong(); } /** * 整型數轉換為標準4位元組寬度 */ public static byte[] intToBytes(int x, ByteOrder order) { ByteBuffer buffer = ByteBuffer.allocate(4); buffer.order(order); buffer.putInt(0, x); return buffer.array(); } /** * 標準4位元組寬度轉換為整型數 */ public static int bytesToInt(byte[] bytes, ByteOrder order) { ByteBuffer buffer = ByteBuffer.allocate(4); buffer.order(order); buffer.put(bytes, 0, bytes.length); buffer.flip(); return buffer.getInt(); }