1. 程式人生 > >談談java中位元組byte有負數的現象

談談java中位元組byte有負數的現象

在研究編碼時,無意中發現java中輸出編碼後的位元組資料的值有的是負值,比如utf-8編碼後的位元組資料,通過遍歷,列印都是負值,java中位元組byte有負數的現象讓我產生了興趣,在此探討一下。

關於編碼的位元組有負數的現象,可以參考這篇部落格:

下面我用java中的資料流去說說這個現象。

實驗一

package com.anjz.test;

import java.io.ByteArrayInputStream;
import java.io.IOException;

public class ByteArrayTest {
	public static void main(String[] args) throws IOException {
		String str = "你好";
		ByteArrayInputStream bis = new ByteArrayInputStream(str.getBytes("utf-8"));
		
		byte[] bytes = new byte[str.getBytes("utf-8").length];
		bis.read(bytes);
		for(byte b :bytes){
			System.out.print(b+",");
		}
	}
}

執行結果:

-28,-67,-96,-27,-91,-67,

實驗二

package com.anjz.test;

import java.io.ByteArrayInputStream;
import java.io.IOException;

public class ByteArrayTest {
	public static void main(String[] args) throws IOException {
		String str = "你好";
		ByteArrayInputStream bis = new ByteArrayInputStream(str.getBytes("utf-8"));
		
		int temp = 0;
		while((temp = bis.read())!=-1){
			System.out.print(temp+",");
		}
	}
}

執行結果:

228,189,160,229,165,189,

實驗一中直接輸出的byte資料,實驗二直接輸出的是int資料,但兩個資料是不一樣的,我們把兩個結果的資料放到一塊。

-28,-67,-96,-27,-91,-67,

228,189,160,229,165,189,

發現一個規律:每列資料的絕對值加一起是個固定值256,這是一個巧合,還是一個規律?關於這個問題,首先我們看一下bis.read()的原始碼。

/**
     * Reads the next byte of data from this input stream. The value
     * byte is returned as an <code>int</code> in the range
     * <code>0</code> to <code>255</code>. If no byte is available
     * because the end of the stream has been reached, the value
     * <code>-1</code> is returned.
     * <p>
     * This <code>read</code> method
     * cannot block.
     *
     * @return  the next byte of data, or <code>-1</code> if the end of the
     *          stream has been reached.
     */
    public synchronized int read() {
        return (pos < count) ? (buf[pos++] & 0xff) : -1;
}

從上述程式碼的說明可以看出,此方法的返回值範圍為[0,255],在方法體中,獲取到位元組後,進行了&0xff操作。

在此說明一個java中的幾個規則:

1. Javabyte的大小是8bitsint的大小是32bitsbyte的範圍是[-128,127],int的範圍是[-231, 231-1]

2. Java中數值的二進位制是採用補碼的形式表示的。

其實從byte和int範圍就可以看出,java中的二進位制是採用補碼錶示的。關於原碼、反碼、補碼的知識,可以參照這篇部落格:

個人理解,計算機不是所有的資料都是需要用補碼錶示的,補碼的出現,主要是將計算機中的減法運算轉化成加法運算,降低計算機底層的複雜性。Java中是數值型別的資料才使用補碼錶示,也就是數值型別在記憶體或磁碟中儲存的都是補碼,程式執行展示的資料是原碼的十進位制(或者說真值)。但對於字元來說,它是通過字符集(如UTF-8、GBK等)進行編碼的,直接儲存位元組數即可。

“你好”UTF-8編碼對應的二進位制:

11100100 10111101 10100000 11100101 10100101 10111101

轉化成byte,當二進位制以數值看待時,記憶體中的二進位制要看成補碼形式。

[11100100] = [10011100]= [-28]十進位制(byte)

[10111101] = [11000011]= [-67]十進位制(byte)

[10100000] = [11100000]= [-96]十進位制(byte)

[11100101] = [10011011]= [-27]十進位制(byte)

[10100101] = [11011011]= [-91]十進位制(byte)

[10111101] = [11000011]= [-67]十進位制(byte)

位元組是計算機最小讀取單位,如果最終轉化成int型別,轉化如下:

[-28]十進位制(byte) = [10011100] = [11100100] ->轉化成32位 [00000000 00000000 00000000 11100100] = [00000000 00000000 00000000 11100100] =[228]十進位制(int)

[-67]十進位制(byte) = [11000011] = [10111101] ->轉化成32位 [00000000 00000000 00000000 10111101] = [00000000 00000000 00000000 10111101] =[189]十進位制(int)

[-96]十進位制(byte) = [11100000] = [10100000] ->轉化成32位 [00000000 00000000 00000000 10100000] = [00000000 00000000 00000000 10100000] =[160]十進位制(int)

[-27]十進位制(byte) = [10011011] = [11100101] ->轉化成32位 [00000000 00000000 00000000 11100101] = [00000000 00000000 00000000 11100101] =[229]十進位制(int)

[-91]十進位制(byte) = [11011011] = [10100101] ->轉化成32位 [00000000 00000000 00000000 10100101] = [00000000 00000000 00000000 10100101] =[165]十進位制(int)

[-67]十進位制(byte) = [11000011] = [10111101] ->轉化成32位 [00000000 00000000 00000000 10111101] = [00000000 00000000 00000000 10111101] =[189]十進位制(int)

首先計算出記憶體中儲存的補碼二進位制,再將值轉化成32位的位元組,高位無值的補0,在將得到的二進位制轉化成原碼,再將原碼轉成十進位制,就是int的資料了。

其實位元組與0xff進行與運算,也可直接轉化成int型別。

0xff我們可以理解它是int型別的,位元組&0xff(補碼和原碼相同),就會強轉成int型別。

位元組-28對應的補碼為11100100,與0xff進行與運算。

     00000000 00000000 00000000 11100100

&  00000000 00000000 00000000 11111111

----------------------------------------------------------------

    00000000 00000000 00000000 11100100

因高位元組最高位為0,原碼和補碼相同,最終int型別

[00000000 00000000 00000000 11100100] = [00000000 00000000 00000000 11100100]= [128]十進位制(int)

因為位元組是8位,當轉化為32位的int型別後,前三個位元組都是0,只有後一個位元組可以是非0的數,故轉化後的int型別的範圍為[0,255]。

按照上面的方式,位元組轉int,直接就展示了記憶體中儲存的補碼對應於無符號的值。一般可以進行其它操作,比如轉化成十六進位制,資料更具有可讀性。

實驗三

package com.anjz.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileStreamTest {
	public static void main(String[] args) throws IOException {
		fileStream(1);
	}
	
	public static void fileStream(int content) throws IOException{
		File f = new File("C:\\Users\\Administrator\\Desktop\\a");
	    FileOutputStream fos = new FileOutputStream(f);
	    fos.write(content);
	    
	    FileInputStream fis = new FileInputStream(f);
	    int a = fis.read();
	    System.out.println(a);
	}
}

通過檔案輸出流,直接將int型別的數字寫入檔案中,再通過檔案輸入流,將內容讀出來。

上述程式碼執行的結果為:1

將上述程式碼fileStream的引數修改為128,執行結果為:128

將上述程式碼fileStream的引數修改為256,執行結果為:0

將上述程式碼fileStream的引數修改為-1,執行結果為:255

上述實驗的結果,有點讓人摸不著頭腦,通過輸入流寫入的數值,通過輸出流讀出的數值,有的是不一樣的,這個是怎麼回事呢?

首先我們看一下fos.write(content);的原始碼:

 /**
     * Writes the specified byte to this file output stream.
     *
     * @param   b   the byte to be written.
     * @param   append   {@code true} if the write operation first
     *     advances the position to the end of file
     */
    private native void write(int b, boolean append) throws IOException;

    /**
     * Writes the specified byte to this file output stream. Implements
     * the <code>write</code> method of <code>OutputStream</code>.
     *
     * @param      b   the byte to be written.
     * @exception  IOException  if an I/O error occurs.
     */
    public void write(int b) throws IOException {
        Object traceContext = IoTrace.fileWriteBegin(path);
        int bytesWritten = 0;
        try {
            write(b, append);
            bytesWritten = 1;
        } finally {
            IoTrace.fileWriteEnd(traceContext, bytesWritten);
        }
}

通過檢視原始碼,並沒有看到什麼特別之處,也沒有看到特別需要注意的說明。但是通過實驗發現一個規律:當寫入的值在[0,255]時,讀出的值也是[0,255],當值不在這個範圍內,讀出的值與寫入的值是不樣的通過觀察,發現[0,255]是一個位元組表示無符號數值的取值範圍。雖然int型別用四個位元組表示的,在這裡,是不是進行了截斷處理呢。按這個思路我們推測一下。

[1]十進位制(int) = [00000000 00000000 00000000 00000001] = [00000000 00000000 00000000 00000001] ->截斷取低8位 [00000001] (寫入的值)->讀取時,轉化成32位[00000000 00000000 00000000 00000001] =[1]十進位制(int)(讀取的值)

[128]十進位制(int)  = [00000000 00000000 00000000 10000000] = [00000000 00000000 00000000 10000000] -> 截斷取低8位 [10000000] (寫入的值)->讀取時,轉化成32位[00000000 00000000 00000000 10000000] = [128]十進位制(int)(讀取的值)

[256]十進位制(int)  = [00000000 00000000 00000001 00000000] = [00000000 00000000 00000001 00000000] -> 截斷取低8位 [00000000](寫入的值)->讀取時,轉化成32位[00000000 00000000 00000000 00000000] = [0]十進位制(int)(讀取的值)

[-1]十進位制(int)  = [10000000 00000000 00000001 00000001] = [11111111 11111111 11111111 11111111] -> 截斷取低8位 [11111111](寫入的值)->讀取時,轉化成32位[00000000 00000000 00000000 11111111] = [255]十進位制(int)(讀取的值)

其實還有一種途徑,去說明存入磁碟中的二進位制是取低8位的補碼,通過notepad++開啟a檔案。

檔案流中輸入:1 ,a檔案展示的是:SOH,輸出流中的值:1

檔案流中輸入:48 ,a檔案中展示的是:0,輸出流中的值:48

檔案流中輸入:65 ,a檔案中展示的是:A,輸出流中的值:65

檔案流中輸入:128 ,a檔案展示的是:x80,輸出流中的值:128

檔案流中輸入:258 ,a檔案展示的是:STX,輸出流中的值:2

通過檢視ASCII表,可以發現檔案中展示的都是ASCII碼對應的字元,可以推測出,檔案存入了一個8位的位元組。

實驗四

package com.anjz.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileStreamTest {
	public static void main(String[] args) throws IOException {
		fileStream(-1);
	}
	
	public static void fileStream(int content) throws IOException{
		File f = new File("C:\\Users\\Administrator\\Desktop\\a");
	    FileOutputStream fos = new FileOutputStream(f);
	    fos.write(content);
	    
	    FileInputStream fis = new FileInputStream(f);
	    byte[] b = new byte[1];
	    fis.read(b, 0, 1);
	    System.out.println(b[0]);
	}
}

上述程式碼,輸入為int型別,輸出為byte型別,看看執行結果為:-1

將上述程式碼fileStream的引數修改為128,執行結果為:-128

將上述程式碼fileStream的引數修改為256,執行結果為:0

將上述程式碼fileStream的引數修改為1000,執行結果為:-24

我們通過實驗三的理論推測一下:

[-1]十進位制(int)  = [10000000 00000000 00000001 00000001] = [11111111 11111111 11111111 11111111] -> 截斷取低8位 [11111111] = [10000001] = [-1]十進位制(byte)

[128]十進位制(int)  = [00000000 00000000 00000000 10000000] = [00000000 00000000 00000000 10000000]  -> 截斷取低8位 [10000000] =[-128]十進位制(byte)這個比較特殊10000000是沒有原碼和反碼的,直接表示最小的數,主要還是因為不存在-0這麼一說)

[256]十進位制(int)  = [00000000 00000000 00000001 00000000] =[00000000 00000000 00000001 00000000] -> 截斷取低8位 [00000000] =[00000000] = [0]十進位制(byte)

[1000]十進位制(int)  = [00000000 00000000 00000011 11101000] = [00000000 00000000 00000011 11101000] -> 截斷取低8位 [11101000] =[10011000] =[-24]十進位制(byte)

從上述實驗得知,在流操作中,如果輸入流寫入的是int型別的值,一般寫入低八位的資料,超出的部分都會被截斷,為了防止寫入的資料和讀取的資料不一樣,建議最好將int型別的範圍控制在[0,127]上,這樣讀取的資料和寫入的資料是一樣的。如果寫入的值不在[0,127]上,資料都是會發生變化的。最好在真實的專案中,直接用byte操作資料,就不會出現int型別轉byte型別,截斷的現象了。

總結

在分析這種問題時,總結了以下幾條規則:

1、位元組是計算機讀取的最小單位。

2、Java中數值是以補碼的形式存在的,應用程式展示的十進位制是補碼對應真值。補碼的存在主要為了簡化計算機底層的運算,將減法運算直接當加法來做。

3、字串的編碼是通過編碼規範直接編碼成二進位制的,如果將編碼後的二進位制轉化成位元組數,就要將這些二進位制當成補碼來看,最終轉化成數值。

4、Java中位元組byte轉化成整型int,可以理解成將有符號數轉化成無符號數,通過擴充套件位數,來達到這種轉化,也可以直接通過公式:位元組數& 0xff實現。

參考的文章: