1. 程式人生 > >byte&oxff到底為什麼【java中的負數】

byte&oxff到底為什麼【java中的負數】

近日需要一個將位元組陣列轉換為十六進位制字串輸出的函式,於是開始編碼如下:

//該程式碼存在問題
public static String byteToHex(byte[] bt){
        StringBuffer sb = new StringBuffer();
        for(int i=0;i<bt.length;i++){
            String tmpStr = Integer.toHexString(byte[i]);
            if(tmpStr.length()<2)
                sb.append("0"
); sb.append(tmpStr); } return sb.toString().toUpperCase(); }

執行測試,一旦遇到負數總出現未可預料的結果,如引數為{-125,125,115,89}時執行結果為FFFFFF837D7359。
開始從網上查資料做修改,對byte資料進行與運算&oxff再進行轉換,很抓狂到底為什麼,再一頓搜尋,原來問題出在java整數表示方法及位擴張問題上,在java中使用補碼對整數進行表示。修改後程式碼如下:

//正確程式碼
public static String byteToHex
(byte[] bt){ StringBuffer sb = new StringBuffer(); for(int i=0;i<bt.length;i++){ int tmp = bt[i]&0xff; String tmpStr = Integer.toHexString(tmp); if(tmpStr.length()<2) sb.append("0"); sb.append(tmpStr); } return
sb.toString().toUpperCase(); }

對於此問題找到了比較全面的解釋如下:

public static void main(String[] args) {
  System.out.println(0xffffffff);
}

下面兩行程式碼的輸出相同嗎?

public static void main(String[] args) {
  byte b=-1;
  System.out.println((int)(char)b);
  System.out.println((int)(char)(b & 0xff));
}

請嘗試在Eclipse中執行上面的兩個程式碼片段,如果你對輸出結果感到很驚訝,請繼續往下讀…

正如你所看到的:
第1個程式碼片段的執行結果是:-1
第2個程式碼片段的執行結果是:65535和255

上面的兩個程式碼片段來源於《Java解惑》的第6個小問題“多重轉型”,原題目內容如下:

public class Multicast{
  public static void main (String[] args){
    System.out.println((int)(char)(byte)-1);
  }
}

上面的程式碼中連續進行了3次型別轉換,最後的結果會回到-1嗎?答案當然是不會,它輸出的結果是65535。下面我為大家整理了相關的基礎知識,相信大家讀完後應該就知道其中的原因了。

一、Java中如何編碼負數?

Java採用”2的補碼“(Two's Complement)編碼負數,它是一種數值的編碼方法,要分二步完成:第一步,每一個二進位制位都取相反值,0變成1,1變成0。比如,+8的二進位制編碼是00001000,取反後就是11110111。第二步,將上一步得到的值加1。11110111就變成11111000。所以,00001000的2的補碼就是11111000。也就是說,-8在計算機(8位機)中就是用11111000表示。關於“2的補碼”的詳細資訊,請參考阮一峰的博文《關於2的補碼》,博文地址附在本文的參考部分。

二、什麼是符號擴充套件(Sign Extension)?

符號擴充套件(Sign Extension)用於在數值型別轉換時擴充套件二進位制位的長度,以保證轉換後的數值和原數值的符號(正或負)和大小相同,一般用於較窄的型別(如byte)向較寬的型別(如int)轉換。擴充套件二進位制位長度指的是,在原數值的二進位制位左邊補齊若干個符號位(0表示正,1表示負)。

舉例來說,如果用6個bit表示十進位制數10,二進位制碼為"00 1010",如果將它進行符號擴充套件為16bits長度,結果是"0000 0000 0000 1010",即在左邊補上10個0(因為10是正數,符號為0),符號擴充套件前後數值的大小和符號都保持不變;如果用10bits表示十進位制數-15,使用“2的補碼”編碼後,二進位制碼為"11 1111 0001",如果將它進行符號擴充套件為16bits,結果是"1111 1111 1111 0001",即在左邊補上6個1(因為-15是負數,符號為1),符號擴充套件前後數值的大小和符號都保持不變。

三、Java型別轉換規則

  1. Java中整型字面量

    Java中int型字面量的書寫方式有以下幾種:

    • 十進位制方式,直接書寫十進位制數字

    • 八進位制方式,格式以0打頭,例如012表示十進位制10

    • 十六進位制方式,格式為0x打頭,例如0xff表示十進位制255

    需要注意的是,在Java中012和0xff返回的都是int型資料,即長度是32位。

  2. Java的數值型別轉換規則

    這個規則是《Java解惑》總結的:如果最初的數值型別是有符號的,那麼就執行符號擴充套件;如果是char型別,那麼不管它要被轉換成什麼型別,都執行零擴充套件。還有另外一條規則也需要記住,如果目標型別的長度小於源型別的長度,則直接擷取目標型別的長度。例如將int型轉換成byte型,直接擷取int型的右邊8位。

四、解析“多重轉型”問題

連續三次型別轉換的表示式如下:

(int)(char)(byte)-1
  1. int(32位) -> byte(8位)

    -1是int型的字面量,根據“2的補碼”編碼規則,編碼結果為0xffffffff,即32位全部置1.轉換成byte型別時,直接擷取最後8位,所以byte結果為0xff,對應的十進位制值是-1.

  2. byte(8位) -> char(16位)

    由於byte是有符號型別,所以在轉換成char型(16位)時需要進行符號擴充套件,即在0xff左邊連續補上8個1(1是0xff的符號位),結果是0xffff。由於char是無符號型別,所以0xffff表示的十進位制數是65535。

  3. char(16位) -> int(32位)

    由於char是無符號型別,轉換成int型時進行零擴充套件,即在0xffff左邊連續補上16個0,結果是0x0000ffff,對應的十進位制數是65535。

五、幾個轉型的例子

在進行型別轉換時,一定要了解表示式的含義,不能光靠感覺。最好的方法是將你的意圖明確表達出來。

在將一個char型數值c轉型為一個寬度更寬的型別時,並且不希望有符號擴充套件,可以如下編碼:

int i = c & 0xffff;

上文曾提到過,0xffff是int型字面量,所以在進行&操作之前,編譯器會自動將c轉型成int型,即在c的二進位制編碼前新增16個0,然後再和0xffff進行&操作,所表達的意圖是強制將前16置0,後16位保持不變。雖然這個操作不是必須的,但是明確表達了不進行符號擴充套件的意圖。
如果需要符號擴充套件,則可以如下編碼:

int i = (short)c; //Cast causes sign extension

首先將c轉換成short型別,它和char是 等寬度的,並且是有符號型別,再將short型別轉換成int型別時,會自動進行符號擴充套件,即如果short為負數,則在左邊補上16個1,否則補上16個0.
如果在將一個byte數值b轉型為一個char時,並且不希望有符號擴充套件,那麼必須使用一個位掩碼來限制它:

char c = (char)(b & 0xff);

(b & 0xff)的結果是32位的int型別,前24被強制置0,後8位保持不變,然後轉換成char型時,直接擷取後16位。這樣不管b是正數還是負數,轉換成char時,都相當於是在左邊補上8個0,即進行零擴充套件而不是符號擴充套件。
如果需要符號擴充套件,則編碼如下:

char c = (char)b; //Sign extension is performed

此時為了明確表達需要符號擴充套件的意圖,註釋是必須的。

六、小結

實際上在數值型別轉換時,只有當遇到負數時才會出現問題,根本原因就是Java中的負數不是採用直觀的方式進行編碼,而是採用“2的補碼”方式,這樣的好處是加法和減法操作可以同時使用加法電路完成,但是在開發時卻會遇到很多奇怪的問題,例如(byte)128的結果是-128,即一個大的正數,截斷後卻變成了負數。3.2節中引用了一些轉型規則,應用這些規則可以很容地解決常見的轉型問題。

七、參考引用