談談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. Java中byte的大小是8bits,int的大小是32bits,byte的範圍是[-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實現。
參考的文章: