JAVA NIO(三):緩衝區的相互轉換及中文亂碼的解決方案
在Java IO中,Channel(通道)只能直接與ByteBuffer進行通訊,這樣我們可能會用ByteBuffer的檢視來解決資料的轉換問題,如將字串轉換為二進位制緩衝區,整數緩衝區轉換為二進位制緩衝區,示例如下:
ByteBuffer buffer = ByteBuffer.allocate(100);
// 獲取緩衝區的檢視,但與ByteBuffer的mark、position、limit互相獨立
CharBuffer charBuff = buffer.asCharBuffer();
// 更容易進行字元操作
charBuff.put("Hello, World!");
// 建立輸出通道
WritableByteChannel outChannel = Channels.newChannel(System.out);
// 寫入緩衝區資料
outChannel.write(byteBuf);
outChannel.close();
採用上述的方法,會導致以下幾個問題:
1. 難以控制字元與位元組的轉換過程;
2. 難以獲取緩衝區的當前位置與剩餘空間;
3. 難以控制緩衝區的階段讀取,如20-50,50-70;
在上面的例子中,我們將CharBuffer與ByteBuffer進行了轉換,極大地簡化了字元的操作行為,但可惜的是,字元依賴於系統編碼(這裡是UTF-8,每個字元佔兩個位元組),寫入的時候每個字元佔兩個位元組,但讀取的時候卻不知道編碼,最後導致出現亂碼。
要解決上述的問題,只有自己控制字元與位元組轉換的過程,才能保證字元的輸入與輸出保持一致,如下:
// 建立輸出通道
WritableByteChannel outChannel = Channels.newChannel(System.out);
String text = "你好,JAVA!";
byte[] arr = text.getBytes("UTF-8");
ByteBuffer byteBuf = ByteBuffer.wrap(arr);
outChannel.write(byteBuf);
outChannel.close();
使用上述的方法,優點是能保證字元的正確轉換,但是很顯然,效率太低了,還有更好的辦法嗎?
經過實測,通過位元組轉換的效率並不低,並且比CharBuffer放入字串的速度高4-5倍,對比資料如下:
位元組數量 | ByteBuffer直接放入資料 | 通過CharBuffer放入資料 |
---|---|---|
1200 | PT0.000087808S | PT0.000437138S |
2400 | PT0.000099591S | PT0.000669771S |
4800 | PT0.000140644S | PT0.000880357S |
這裡最有意思的地方在於,CharBuffer統一以兩個位元組儲存一個字元,而ByteBuffer則依賴於編碼,可能是兩個位元組,也可能是三個位元組,最後導致512個字元,CharBuffer的limit為512,換算為ByteBuffer後為1024個位元組,而直接用ByteBuffer儲存位元組則為1200(依賴於具體的字元,不定),如下:
字元數量 | ByteBuffer直接放入資料 | 通過CharBuffer放入資料 |
---|---|---|
512 | 1200 | 512 |
1024 | 2400 | 1024 |
2048 | 4800 | 1024 |
關於亂碼,如果是以CharBuffer放入的資料,因為沒有對應的字符集解碼器,必定是亂碼,只有通過同樣的CharBuufer方式讀出,才能正確解決,所以在這裡,char是一種資料儲存格式,就如同long資料、int資料一樣,並不是作業系統上對應的字符集,所以無法解析。尤其是文字編輯器提示“此文件包含當前文字編碼無法處理的字元”,則可以斷定是以字元資料格式進行儲存,處理方式如下:
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteBuffer bbuf = ByteBuffer.allocateDirect(1024);
long offset = 0;
long nums = 0;
while((nums = fc.read(bbuf, offset)) > 0) {
// 一定要反轉回到正確位置,CharBuffer檢視是基於當前位置建立的
bbuf.flip();
// 以字元資料方式進行讀取
CharBuffer cbuf = bbuf.asCharBuffer();
bbuf.clear();
offset = offset + nums;
}
fc.close();
fis.close();
結論
- 一定要區分字元與字元資料,如果是作業系統上的字元,則可對應到文字編碼,並有相應的字符集編碼,而字元資料必定是二進位制編碼;
- 緩衝區儲存的是位元組(不是字元);
- 字元由多個位元組組成,如果不能按照指定的順序與數量讀取所需的位元組,必然出現亂碼。