java中文亂碼解決之道(五)-----java是如何編碼解碼的,java-----java
在上篇部落格中LZ闡述了java各個渠道轉碼的過程,闡述了java在運行過程中那些步驟在進行轉碼,在這些轉碼過程中如果一處出現問題就很有可能會產生亂碼!下面LZ就講述java在轉碼過程中是如何來進行編碼和解碼操作的。
編碼&解碼
在上篇部落格中LZ闡述了三個渠道的編碼轉換過程,下面LZ將結束java在那些場合需要進行編碼和解碼操作,並詳序中間的過程,進一步掌握java的編碼和解碼過程。在java中主要有四個情境需要進行編碼解碼操作:
1:I/O操作
2:記憶體
3:資料庫
4:javaWeb
下面主要介紹前面兩種情境,資料庫部分只要設定正確編碼格式就不會有什麼問題,javaWeb情境過多需要瞭解URL、get、POST的編碼,servlet的解碼,所以javaWeb情境下節LZ介紹。
I/O操作
在前面LZ就提過亂碼問題無非就是轉碼過程中編碼格式的不統一產生的,比如編碼時採用UTF-8,解碼採用GBK,但最根本的原因是字元到位元組或者位元組到字元的轉換出問題了,而這中情況的轉換最主要的情境就是I/O操作的時候。當然I/O操作主要包括網路I/O(也就是javaWeb)和磁碟I/O。網路I/O下節介紹。
首先我們先看I/O的編碼操作。
InputStream為位元組輸入資料流的所有類的超類,Reader為讀取字元流的抽象類別。java讀取檔案的方式分為按位元組流讀取和按字元流讀取,其中InputStream、Reader是這兩種讀取方式的超類。
按位元組
我們一般都是使用InputStream.read()方法在資料流中讀取位元組(read()每次都唯讀取一個位元組,效率非常慢,我們一般都是使用read(byte[])),然後儲存在一個byte[]數組中,最後轉換為String。在我們讀取檔案時,讀取位元組的編碼取決於檔案所使用的編碼格式,而在轉換為String過程中也會涉及到編碼的問題,如果兩者之間的編碼格式不同可能會出現問題。例如存在一個問題test.txt編碼格式為UTF-8,那麼通過位元組流讀取檔案時所獲得的資料流編碼格式就是UTF-8,而我們在轉化成String過程中如果不指定編碼格式,則預設使用系統編碼格式(GBK)來解碼操作,由於兩者編碼格式不一致,那麼在構造String過程肯定會產生亂碼,如下:
File file = new File("C:\\test.txt"); InputStream input = new FileInputStream(file); StringBuffer buffer = new StringBuffer(); byte[] bytes = new byte[1024]; for(int n ; (n = input.read(bytes))!=-1 ; ){ buffer.append(new String(bytes,0,n)); } System.out.println(buffer);
輸出結果:鍩挎垜鏄?cm
test.txt中的內容為:我是 cm。
要想不出現亂碼,在構造String過程中指定編碼格式,使得編碼解碼時兩者編碼格式保持一致即可:
buffer.append(new String(bytes,0,n,"UTF-8"));
按字元
其實字元流可以看做是一種封裝流,它的底層還是採用位元組流來讀取位元組,然後它使用指定的編碼方式將讀取位元組解碼為字元。在java中Reader是讀取字元流的超類。所以從底層上來看按位元組讀取檔案和按字元讀取沒什麼區別。在讀取的時候字元讀取每次是讀取留個位元組,位元組流每次讀取一個位元組。
位元組&字元轉換
位元組轉換為字元一定少不了InputStreamReader。API解釋如下:InputStreamReader 是位元組流通向字元流的橋樑:它使用指定的 charset 讀取位元組並將其解碼為字元。它使用的字元集可以由名稱指定或顯式給定,或者可以接受平台預設的字元集。 每次調用 InputStreamReader 中的一個 read() 方法都會導致從底層輸入資料流讀取一個或多個位元組。要啟用從位元組到字元的有效轉換,可以提前從底層流讀取更多的位元組,使其超過滿足當前讀取操作所需的位元組。API解釋非常清楚,InputStreamReader在底層讀取檔案時仍然採用位元組讀取,讀取位元組後它需要根據一個指定的編碼格式來解析為字元,如果沒有指定編碼格式則採用系統預設編碼格式。
String file = "C:\\test.txt"; String charset = "UTF-8"; // 寫字元換轉成位元組流 FileOutputStream outputStream = new FileOutputStream(file); OutputStreamWriter writer = new OutputStreamWriter(outputStream, charset); try { writer.write("我是 cm"); } finally { writer.close(); } // 讀取位元組轉換成字元 FileInputStream inputStream = new FileInputStream(file); InputStreamReader reader = new InputStreamReader( inputStream, charset); StringBuffer buffer = new StringBuffer(); char[] buf = new char[64]; int count = 0; try { while ((count = reader.read(buf)) != -1) { buffer.append(buf, 0, count); } } finally { reader.close(); } System.out.println(buffer);
記憶體
首先我們看下面這段簡單的代碼
String s = "我是 cm"; byte[] bytes = s.getBytes(); String s1 = new String(bytes,"GBK"); String s2 = new String(bytes);
在這段代碼中我們看到了三處編碼轉換過程(一次編碼,兩次解碼)。先看String.getTytes():
public byte[] getBytes() { return StringCoding.encode(value, 0, value.length); }
內部調用StringCoding.encode()方法操作:
static byte[] encode(char[] ca, int off, int len) { String csn = Charset.defaultCharset().name(); try { // use charset name encode() variant which provides caching. return encode(csn, ca, off, len); } catch (UnsupportedEncodingException x) { warnUnsupportedCharset(csn); } try { return encode("ISO-8859-1", ca, off, len); } catch (UnsupportedEncodingException x) { // If this code is hit during VM initialization, MessageUtils is // the only way we will be able to get any kind of error message. MessageUtils.err("ISO-8859-1 charset not available: " + x.toString()); // If we can not find ISO-8859-1 (a required encoding) then things // are seriously wrong with the installation. System.exit(1); return null; } }
encode(char[] paramArrayOfChar, int paramInt1, int paramInt2)方法首先調用系統的預設編碼格式,如果沒有指定編碼格式則預設使用ISO-8859-1編碼格式進行編碼操作,進一步深入如下:
String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
同樣的方法可以看到new String 的建構函式內部是調用StringCoding.decode()方法:
public String(byte bytes[], int offset, int length, Charset charset) { if (charset == null) throw new NullPointerException("charset"); checkBounds(bytes, offset, length); this.value = StringCoding.decode(charset, bytes, offset, length); }
decode方法和encode對編碼格式的處理是一樣的。
對於以上兩種情況我們只需要設定統一的編碼格式一般都不會產生亂碼問題。
編碼&編碼格式
首先先看看java編碼類別圖[1]
首先根據指定的chart設定ChartSet類,然後根據ChartSet建立ChartSetEncoder對象,最後再調用 CharsetEncoder.encode 對字串進行編碼,不同的編碼類別型都會對應到一個類中,實際的編碼過程是在這些類中完成的。下面時序圖展示詳細的編碼過程:
通過這編碼的類圖和時序圖可以瞭解編碼的詳細過程。下面將通過一段簡單的代碼對ISO-8859-1、GBK、UTF-8編碼
public class Test02 { public static void main(String[] args) throws UnsupportedEncodingException { String string = "我是 cm"; Test02.printChart(string.toCharArray()); Test02.printChart(string.getBytes("ISO-8859-1")); Test02.printChart(string.getBytes("GBK")); Test02.printChart(string.getBytes("UTF-8")); } /** * char轉換為16進位 */ public static void printChart(char[] chars){ for(int i = 0 ; i < chars.length ; i++){ System.out.print(Integer.toHexString(chars[i]) + " "); } System.out.println(""); } /** * byte轉換為16進位 */ public static void printChart(byte[] bytes){ for(int i = 0 ; i < bytes.length ; i++){ String hex = Integer.toHexString(bytes[i] & 0xFF); if (hex.length() == 1) { hex = '0' + hex; } System.out.print(hex.toUpperCase() + " "); } System.out.println(""); }}-------------------------outPut:6211 662f 20 63 6d 3F 3F 20 63 6D CE D2 CA C7 20 63 6D E6 88 91 E6 98 AF 20 63 6D
通過程式我們可以看到“我是 cm”的結果為:
char[]:6211 662f 20 63 6d
ISO-8859-1:3F 3F 20 63 6D
GBK:CE D2 CA C7 20 63 6D
UTF-8:E6 88 91 E6 98 AF 20 63 6D
圖如下:
更多&參考文獻
對於這兩種情境我們只需要設定一致正確的編碼一般都不會產生亂碼問題,通過LZ上面的闡述對於java編碼解碼的過程應該會有一個比較清楚的認識。其實在java中產生亂碼的主要情境是在javaWeb中,所以LZ下篇博文就來講解javaWeb中的亂碼產生情形。
1、Java 編程技術中漢字問題的分析及解決:http://www.ibm.com/developerworks/cn/java/java_chinese/。
-----原文出自:http://cmsblogs.com/?p=1491,請尊重作者辛勤勞動成果,轉載說明出處.
-----個人網站:http://cmsblogs.com