面試之Java String 編碼相關

鍋叔發表於2022-03-30

  另有一篇我的字元編碼本質入門的文章見這裡: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]

  我只能回答成這樣了,自我感覺比較風流倜儻,瀟灑惆悵的可以先自己琢磨下, 實際的程式輸出在這裡↓

面試之Java String 編碼相關
 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.

相關文章