另有一篇我的字元編碼本質入門的文章見這裡:https://www.cnblogs.com/uncleguo/p/16008551.html
實話說,作為一個多年Java老年程式設計師,直到近來,在沒有決心花時間搞清楚Java String的編碼相關問題之前, 自己也都還是似懂非懂,一臉懵逼的。設想如果在面試中,有同學能夠條理清晰的回答下面的問題,那必是非常了得之人,論智慧武功應該均在本人之上:-)。
問:請預測下面程式的輸出,並解釋原因。printHexBinary方法為16進位制列印Byte
1 String str = "中"; 2 3 byte[] bufferGBK = str.getBytes("GBK"); 4 System.out.println("bufferGBK = "+printHexBinary(bufferGBK)) ; 5 6 String gbkString =new String(bufferGBK,"GBK"); 7 System.out.println("gbkString = new String bufferGBK GBK : "+gbkString); 8 9 String utf8String =new String(bufferGBK,"utf-8"); 10 System.out.println("utf8String = new String bufferGBK utf8 : "+utf8String); 11 12 byte[] utfFromStr = utf8String.getBytes("utf-8"); 13 System.out.println("utf8String getBytes utf-8 : "+printHexBinary(utfFromStr)); 14 15 byte[] gbkFromStr = utf8String.getBytes("GBK"); 16 System.out.println("utf8String getBytes GBK : "+printHexBinary(gbkFromStr)); 17 18 byte[] isoFromStr = utf8String.getBytes("ISO-8859-1"); 19 System.out.println("utf8String getBytes ISO-8859-1 : "+printHexBinary(isoFromStr)); 20 21 String isoString =new String(bufferGBK,"ISO-8859-1"); 22 System.out.println("isoString = new String bufferGBK ISO-8859-1 : "+isoString); 23 24 utfFromStr = isoString.getBytes("utf-8"); 25 System.out.println("isoString getBytes utf-8 : "+printHexBinary(utfFromStr)); 26 27 gbkFromStr = isoString.getBytes("GBK"); 28 System.out.println("isoString getBytes GBK : "+printHexBinary(gbkFromStr)); 29 30 isoFromStr = isoString.getBytes("ISO-8859-1"); 31 System.out.println("isoString getBytes ISO-8859-1 : "+printHexBinary(isoFromStr));
按我之前的認識,先簡單推理下。
第4行的Print輸出的應該是“中”的GBK編碼(中的GBK編碼是0xD6 0xD0)。
第7行用[0xD6 0xD0]以GBK字符集new一個String,列印這個String,那應該是“中”
第10行用[0xD6 0xD0]以UTF8字符集new一個String,列印這個String,這裡可能會亂碼,具體會顯示什麼字元,要看0xD6 0xD0對應的Utf8 字元。
× 第13行從上面new的String中按UTF8取得Byte陣列,因為上面New 的是Utf8 String,這裡取出的應該還是[0xD6 0xD0]
× 第16行從上面new的String中按GBK取得Byte陣列, 這……不太確定,可能還是[0xD6 0xD0]?記憶體儲存的編碼應該是不變的?
× 第19行從上面new的String中按ISO8859取得Byte陣列, 這……同上吧? 但似乎有點兒問題,應該是不對,邏輯上如果getBytes都一樣,那為啥要引數指定字符集呢?
第22行用[0xD6 0xD0]以ISO8859字符集new一個String,列印這個String,這裡可能會亂碼, 要看[0xD6 0xD0]ISO8859中對應的字元。
× 第25,28行,這……
第30行從上面new的String中按ISO8859取得Byte陣列,這應該不會變,還是[0xD6 0xD0]
我只能回答成這樣了,自我感覺比較風流倜儻,瀟灑惆悵的可以先自己琢磨下, 實際的程式輸出在這裡↓
1 ======================================== 2 bufferGBK = 0xD6,0xD0 3 gbkString = new String bufferGBK GBK : 中 4 utf8String = new String bufferGBK utf8 : �� 5 utf8String getBytes utf-8 : 0xEF,0xBF,0xBD,0xEF,0xBF,0xBD 6 utf8String getBytes GBK : 0x3F,0x3F 7 utf8String getBytes ISO-8859-1 : 0x3F,0x3F 8 isoString = new String bufferGBK ISO-8859-1 : ÖÐ 9 isoString getBytes utf-8 : 0xC3,0x96,0xC3,0x90 10 isoString getBytes GBK : 0x3F,0x3F 11 isoString getBytes ISO-8859-1 : 0xD6,0xD0 12 ========================================
然後對著輸出結果來理解下。
答案中的2,3行輸出跟預期一樣
第4行確實是“亂碼”了,但為什麼[0xD6 0xD0]會變成兩個一樣的字元��
第5行,byte陣列不是之前的2個,而是6個元素,與0xD6 0xD0完全不同,是何原因?
第6,7行,byte陣列是[0x3F 0x3F],為啥?
第8行,也是“亂碼”了,ÖÐ, 但為什麼又變成了兩個不同的字元。。-_-||
第9行 byte陣列4個元素,看起來不同。
第10行 byte陣列[0x3f 0x3f]
第11行 確實還是[0xD6 0xD0]
實踐檢驗真理,上面的實驗表明,String在記憶體儲存的實際內容與getBytes取得的內容,可能是存在轉換關係的。某些字符集的情況下是不變的(ISO8859),而有些經過Byte 到 String 到 Byte 的轉換後會發生變化,與建立時的byte陣列不同。
經過一番上下求索之後。下面是我認為比較合理的解釋。
答案中的2,3行輸出跟預期一樣
第4行,亂碼因為[0xD6 0xD0]不是兩個有效的Utf8字符集字元, Java將其轉換處理為兩個�,即utf8String中的內容即為“��”
第5行此時取得Byte陣列為對應Utf8 中兩個�字元的字元編碼,即在UTF8 字符集中� 的編碼為[0xEF,0xBF,0xBD]
第6行取得的Byte陣列為,字元�對應在GBK字符集中的字元編碼,該字元應該未包含,被轉換為 0x3F 即 ? 字元
第7行,同上
第8行,並不是亂碼,Ö 和 Ð 確實是ISO8859字符集中包含的字元,對應的編碼為[0xD6 0xD0],在GBK中為字元 “中” ,在 ISO8859中為兩個字元 “Ö” 和 “Д,isoString內容為“ÖД
第9行,取得isoString在utf8 編碼集中對應 Ö 和 Ð 字元的編碼陣列, 即 [0xC3,0x96] =Ö [0xC3,0x90] = Ð。
第10行,取得isoString在GBK編碼其中對應的Ö 和 Ð 字元的編碼陣列,因為GBK未包含這兩個字元,於是被轉換為“??”後取得編碼 即 [0x3F 0x3F]
第10行,取得isoString在ISO8859中對應的Ö 和 Ð 字元的編碼陣列,即為[0xD6 0xD0],因此不變。
總結及推論:
- String實際儲存的內容是不可見,也無需關心的,可以理解為它儲存的是字元。你用Byte陣列初始化一個字串時,總會顯示或者預設的指明陣列的編碼格式。String內部會據此將其對應的字元而非編碼,以某種方法儲存在其內部。如果你指定的字符集與提供的陣列不一致,String會幫你對映為未知字元可能是“?”或“�”。
- String儲存的不是初始化時提供的Byte陣列,因此經過 Byte 到 String的轉換後,可能會導致原始Byte陣列的內容丟失,無法通過轉換後的 String獲得。所以亂碼問題,要從源頭解決,而不是在String上下功夫。
- ISO8859-1是一個0x00-0xFF的都有定義的單字元編碼,因此該編碼進行byte到String轉換不會丟失資訊,String可以以Iso8859取得Byte陣列後,以其他字符集顯示,因此很多地方仍然使用此種字符集。
另:字元是抽象的,具體儲存肯定要定義編碼,Java規範定義的是“外部”的編碼的表現和工作方式,內部儲存可以自行實現,目前實際使用似乎是UTF16.