JAVA字符編碼問題

一、基礎概念
1、 ANSI編碼方式包括GBK、GB2312
2、 UTF-8編碼方式ISO-10646-1
3、 IE瀏覽器正常(默認)情況下只解析以ANSI方式存儲的中文,否則出現亂碼。同時在解析中文較多的網頁時ANSI(GB2312)方式最快。
4、 如果採用FileWriter類輸出,使用編碼方式(UFT-8)(winxp,win7下)輸出。
5、 如果採用OutputStreamWriter((new FileOutputStream(f)),"GB2312")方式強制輸出爲ANSI編碼方式。
以下是在Html中中文亂碼問題的解決代碼!
-----------------------------------------------------------------------------------------------
二、解釋
 字節流與字符流主要的區別是他們的的處理方式
字節流是最基本的,所有的InputStream和OutputStream的子類都是,主要用在處理二進制數據,它是按字節來處理的
但實際中很多的數據是文本,又提出了字符流的概念,它是按虛擬機的encode來處理,也就是要進行字符集的轉化
這兩個之間通過 InputStreamReader,OutputStreamWriter來關聯,實際上是通過byte[]和String來關聯
在實際開發中出現的漢字問題實際上都是在字符流和字節流之間轉化不統一而造成的

在從字節流轉化爲字符流時,實際上就是byte[]轉化爲String時,
public String(byte bytes[], String charsetName)
有一個關鍵的參數字符集編碼,通常我們都省略了,那系統就用操作系統的lang
而在字符流轉化爲字節流時,實際上是String轉化爲byte[]時,
byte[]    String.getBytes(String charsetName)
也是一樣的道理

至於java.io中還出現了許多其他的流,按主要是爲了提高性能和使用方便,
如BufferedInputStream,PipedInputStream等
------------------------------------------------------------------------------------------------

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;


public class CharsetTest {//強制使用ANSI方式存儲文件
    public static void main(String[] args) throws Exception {
        File f=new File("C:\\f1.html");
        String str="<html><head><title>"+"哈子飛!"+"</title></head><body>"+"哈子飛" +".html"+"</body></html>";
        OutputStreamWriter osw=new OutputStreamWriter((new FileOutputStream(f)),"GB2312");
        //OutputStreamWriter 是字符流通向字節流的橋樑
        //FileOutputStream此抽象類是表示輸出字節流的所有類的超類。

        //輸出流接受輸出字節並將這些字節發送到某個接收器。
        osw.write(str);
        osw.flush();
        osw.close();
    }
}

三、正確的做字符串編碼轉換

1.字符串的內部表示?

字符串在java中統一用unicode表示( 即utf-16 LE) , 

對於 String s = "你好哦!";

如果源碼文件是GBK編碼, 操作系統(windows)默認的環境編碼爲GBK,那麼編譯時,  JVM將 按照GBK編碼將字節數組解析成字符,然後將字符轉換爲unicode格式的字節數組,作爲內部存儲。

當打印這個字符串時,JVM 根據操作系統本地的語言環境,將unicode轉換爲GBK,然後操作系統將GBK格式的內容顯示出來。

 

當源碼文件是UTF-8, 我們需要通知編譯器源碼的格式,javac -encoding utf-8 ... , 編譯時,JVM按照utf-8 解析成字符,然後轉換爲unicode格式的字節數組, 那麼不論源碼文件是什麼格式,同樣的字符串,最後得到的unicode字節數組是完全一致的,顯示的時候,也是轉成GBK來顯示(跟OS環境有關)

 

2.亂碼如何產生? 本質上都是由於 字符串原本的編碼格式 與 讀取時解析用的編碼格式不一致導致的。

例如:

String s = "你好哦!";

System.out.println( new String(s.getBytes(),"UTF-8")); //錯誤,因爲getBytes()默認使用GBK編碼, 而解析時使用UTF-8編碼,肯定出錯。

其中 getBytes() 是將unicode 轉換爲操作系統默認的格式的字節數組,即"你好哦"的 GBK格式,

new String (bytes, Charset) 中的charset 是指定讀取 bytes 的方式,這裏指定爲UTF-8,即把bytes的內容當做UTF-8 格式對待。

如下兩種方式都會有正確的結果,因爲他們的源內容編碼和解析用的編碼是一致的。

System.out.println( new String(s.getBytes(),"GBK"));

System.out.println( new String(s.getBytes("UTF-8"),"UTF-8"));

 

那麼,如何利用getBytes 和 new String() 來進行編碼轉換呢?  網上流傳着一種錯誤的方法:

GBK--> UTF-8:    new String( s.getBytes("GBK") , "UTF-8);   ,這種方式是完全錯誤的,因爲getBytes 的編碼與  UTF-8 不一致,肯定是亂碼。

但是爲什麼在tomcat 下,使用 new String(s.getBytes("iso-8859-1") ,"GBK") 卻可以用呢? 答案是:

tomcat 默認使用iso-8859-1編碼, 也就是說,如果原本字符串是GBK的,tomcat傳輸過程中,將GBK轉成iso-8859-1了,

默認情況下,使用iso-8859-1讀取中文肯定是有問題的,那麼我們需要將iso-8859-1 再轉成GBK, 而iso-8859-1 是單字節編碼的,

即他認爲一個字節是一個字符, 那麼這種轉換不會對原來的字節數組做任何改變,因爲字節數組本來就是由單個字節組成的,

如果之前用GBK編碼,那麼轉成iso-8859-1後編碼內容完全沒變, 則 s.getBytes("iso-8859-1")  實際上還是原來GBK的編碼內容

則 new String(s.getBytes("iso-8859-1") ,"GBK")  就可以正確解碼了。 所以說這是一種巧合。

 

3.如何正確的將GBK轉UTF-8 ? (實際上是unicode轉UTF-8)

String gbkStr = "你好哦!"; //源碼文件是GBK格式,或者這個字符串是從GBK文件中讀取出來的, 轉換爲string 變成unicode格式

//利用getBytes將unicode字符串轉成UTF-8格式的字節數組

byte[] utf8Bytes = gbkStr.getBytes("UTF-8"); 

//然後用utf-8 對這個字節數組解碼成新的字符串

String utf8Str = new String(utf8Bytes, "UTF-8");

簡化後就是:

unicodeToUtf8 (String s) {

return new String( s.getBytes("utf-8") , "utf-8");

}

UTF-8 轉GBK原理也是一樣

return new String( s.getBytes("GBK") , "GBK");

 

 其實核心工作都由  getBytes(charset) 做了。

getBytes 的JDK 描述:Encodes this String into a sequence of bytes using the named charset, storing the result into a new byte array.

 

另外對於讀寫文件,

OutputStreamWriter w1 = new OutputStreamWriter(new FileOutputStream("D:\\file1.txt"),"UTF-8");

InputStreamReader( stream, charset)

可以幫助我們輕鬆的按照指定編碼讀寫文件。


四、字符串的各種編碼轉換

/**
 * 轉換字符串的編碼
 */
public class ChangeCharset {
 /** 7位ASCII字符,也叫作ISO646-US、Unicode字符集的基本拉丁塊 */
 public static final String US_ASCII = "US-ASCII";

 /** ISO 拉丁字母表 No.1,也叫作 ISO-LATIN-1 */
 public static final String ISO_8859_1 = "ISO-8859-1";

 /** 8 位 UCS 轉換格式 */
 public static final String UTF_8 = "UTF-8";

 /** 16 位 UCS 轉換格式,Big Endian(最低地址存放高位字節)字節順序 */
 public static final String UTF_16BE = "UTF-16BE";

 /** 16 位 UCS 轉換格式,Little-endian(最高地址存放低位字節)字節順序 */
 public static final String UTF_16LE = "UTF-16LE";

 /** 16 位 UCS 轉換格式,字節順序由可選的字節順序標記來標識 */
 public static final String UTF_16 = "UTF-16";

 /** 中文超大字符集 */
 public static final String GBK = "GBK";

 /**
  * 將字符編碼轉換成US-ASCII碼
  */
 public String toASCII(String str) throws UnsupportedEncodingException{
  return this.changeCharset(str, US_ASCII);
 }
 /**
  * 將字符編碼轉換成ISO-8859-1碼
  */
 public String toISO_8859_1(String str) throws UnsupportedEncodingException{
  return this.changeCharset(str, ISO_8859_1);
 }
 /**
  * 將字符編碼轉換成UTF-8碼
  */
 public String toUTF_8(String str) throws UnsupportedEncodingException{
  return this.changeCharset(str, UTF_8);
 }
 /**
  * 將字符編碼轉換成UTF-16BE碼
  */
 public String toUTF_16BE(String str) throws UnsupportedEncodingException{
  return this.changeCharset(str, UTF_16BE);
 }
 /**
  * 將字符編碼轉換成UTF-16LE碼
  */
 public String toUTF_16LE(String str) throws UnsupportedEncodingException{
  return this.changeCharset(str, UTF_16LE);
 }
 /**
  * 將字符編碼轉換成UTF-16碼
  */
 public String toUTF_16(String str) throws UnsupportedEncodingException{
  return this.changeCharset(str, UTF_16);
 }
 /**
  * 將字符編碼轉換成GBK碼
  */
 public String toGBK(String str) throws UnsupportedEncodingException{
  return this.changeCharset(str, GBK);
 }
 
 /**
  * 字符串編碼轉換的實現方法
  * @param str  待轉換編碼的字符串
  * @param newCharset 目標編碼
  * @return
  * @throws UnsupportedEncodingException
  */
 public String changeCharset(String str, String newCharset)
   throws UnsupportedEncodingException {
  if (str != null) {
   //用默認字符編碼解碼字符串。
   byte[] bs = str.getBytes();
   //用新的字符編碼生成字符串
   return new String(bs, newCharset);
  }
  return null;
 }
 /**
  * 字符串編碼轉換的實現方法
  * @param str  待轉換編碼的字符串
  * @param oldCharset 原編碼
  * @param newCharset 目標編碼
  * @return
  * @throws UnsupportedEncodingException
  */
 public String changeCharset(String str, String oldCharset, String newCharset)
   throws UnsupportedEncodingException {
  if (str != null) {
   //用舊的字符編碼解碼字符串。解碼可能會出現異常。
   byte[] bs = str.getBytes(oldCharset);
   //用新的字符編碼生成字符串
   return new String(bs, newCharset);
  }
  return null;
 }

 public static void main(String[] args) throws UnsupportedEncodingException {
  ChangeCharset test = new ChangeCharset();
  String str = "This is a 中文的 String!";
  System.out.println("str: " + str);
  String gbk = test.toGBK(str);
  System.out.println("轉換成GBK碼: " + gbk);
  System.out.println();
  String ascii = test.toASCII(str);
  System.out.println("轉換成US-ASCII碼: " + ascii);
  gbk = test.changeCharset(ascii,ChangeCharset.US_ASCII, ChangeCharset.GBK);
  System.out.println("再把ASCII碼的字符串轉換成GBK碼: " + gbk);
  System.out.println();
  String iso88591 = test.toISO_8859_1(str);
  System.out.println("轉換成ISO-8859-1碼: " + iso88591);
  gbk = test.changeCharset(iso88591,ChangeCharset.ISO_8859_1, ChangeCharset.GBK);
  System.out.println("再把ISO-8859-1碼的字符串轉換成GBK碼: " + gbk);
  System.out.println();
  String utf8 = test.toUTF_8(str);
  System.out.println("轉換成UTF-8碼: " + utf8);
  gbk = test.changeCharset(utf8,ChangeCharset.UTF_8, ChangeCharset.GBK);
  System.out.println("再把UTF-8碼的字符串轉換成GBK碼: " + gbk);
  System.out.println();
  String utf16be = test.toUTF_16BE(str);
  System.out.println("轉換成UTF-16BE碼:" + utf16be);
  gbk = test.changeCharset(utf16be,ChangeCharset.UTF_16BE, ChangeCharset.GBK);
  System.out.println("再把UTF-16BE碼的字符串轉換成GBK碼: " + gbk);
  System.out.println();
  String utf16le = test.toUTF_16LE(str);
  System.out.println("轉換成UTF-16LE碼:" + utf16le);
  gbk = test.changeCharset(utf16le,ChangeCharset.UTF_16LE, ChangeCharset.GBK);
  System.out.println("再把UTF-16LE碼的字符串轉換成GBK碼: " + gbk);
  System.out.println();
  String utf16 = test.toUTF_16(str);
  System.out.println("轉換成UTF-16碼:" + utf16);
  gbk = test.changeCharset(utf16,ChangeCharset.UTF_16LE, ChangeCharset.GBK);
  System.out.println("再把UTF-16碼的字符串轉換成GBK碼: " + gbk);
  String s = new String("中文".getBytes("UTF-8"),"UTF-8");
  System.out.println(s);
 }
}

------------------------------------------------------------------------------------------------------------------


        java中的String類是按照unicode進行編碼的,當使用String(byte[] bytes, String encoding)構造字符串時,encoding所指的是bytes中的數據是按照那種方式編碼的,而不是最後產生的String是什麼編碼方式,換句話說,是讓系統把bytes中的數據由encoding編碼方式轉換成unicode編碼。如果不指明,bytes的編碼方式將由jdk根據操作系統決定。

        當我們從文件中讀數據時,最好使用InputStream方式,然後採用String(byte[] bytes, String encoding)指明文件的編碼方式。不要使用Reader方式,因爲Reader方式會自動根據jdk指明的編碼方式把文件內容轉換成unicode編碼。

        當我們從數據庫中讀文本數據時,採用ResultSet.getBytes()方法取得字節數組,同樣採用帶編碼方式的字符串構造方法即可。

ResultSet rs;
bytep[] bytes = rs.getBytes();
String str = new String(bytes, "gb2312");

不要採取下面的步驟。

ResultSet rs;
String str = rs.getString();
str = new String(str.getBytes("iso8859-1"), "gb2312");

        這種編碼轉換方式效率底。之所以這麼做的原因是,ResultSet在getString()方法執行時,默認數據庫裏的數據編碼方式爲iso8859-1。系統會把數據依照iso8859-1的編碼方式轉換成unicode。使用str.getBytes("iso8859-1")把數據還原,然後利用new String(bytes, "gb2312")把數據從gb2312轉換成unicode,中間多了好多步驟。

        從HttpRequest中讀參數時,利用reqeust.setCharacterEncoding()方法設置編碼方式,讀出的內容就是正確的了。

五 、漢字爲奇數時亂碼

問題出現:GBK轉UTF-8時,奇數箇中文會亂碼,偶數箇中文不會亂碼。
三個中文

Java代碼  收藏代碼
  1. public static void encodeError() throws UnsupportedEncodingException {  
  2.     String gbk = "我來了";  
  3.     String utf8 = new String(gbk.getBytes("UTF-8"));  
  4.   
  5.     //模擬UTF-8編碼的網站顯示  
  6.     System.out.println(new String(utf8.getBytes(),"UTF-8"));  
  7. }  
  8. /* 
  9. 我來?? 
  10. */  

 前面三個中文,後面一箇中文,都是奇數

Java代碼  收藏代碼
  1. public static void encodeError2() throws UnsupportedEncodingException {  
  2.     String gbk = "今年是2011年";  
  3.     String utf8 = new String(gbk.getBytes("UTF-8"));  
  4.   
  5.     //模擬UTF-8編碼的網站顯示  
  6.     System.out.println(new String(utf8.getBytes(),"UTF-8"));  
  7. }  
  8. /* 
  9. 今年??011?? 
  10. */  

 

原因:爲什麼只有奇數箇中文才亂碼,偶數個卻不亂碼?下面來分析原因

Java代碼  收藏代碼
  1. public static void analyze() throws UnsupportedEncodingException {  
  2.     String gbk = "我來了";  
  3.     String utf8 = new String(gbk.getBytes("UTF-8"));  
  4.     for (byte b : gbk.getBytes("UTF-8")) {  
  5.         System.out.print(b + " ");  
  6.     }  
  7.     System.out.println();  
  8.     for (byte b : utf8.getBytes()) {  
  9.         System.out.print(b + " ");  
  10.     }  
  11. }  
  12. /* 
  13. -26 -120 -111 -26 -99 -91 -28 -70 -122  
  14. -26 -120 -111 -26 -99 -91 -28 -70 63  
  15. */  

 注意最後一個字節不同,上面一行纔是正確的UTF-8編碼。那麼爲什麼下面一行最後一個字節是63,而不是-122呢?這就是導致亂碼的原因所在。
GBK編碼是一箇中文2個字節,而UTF-8編碼是一箇中文3個字節,當我們調用getBytes("UTF-8")方法時,會通過計算來增加字節,使得從GBK的2個字節變成UTF-8對應的3個字節。因此,上例3箇中文輸出了9個字節。

 

這裏講一下怎麼通過計算增加字節,不深究的讀者可以跳過此段。爲了醒目,直接用代碼講解

Java代碼  收藏代碼
  1. public static void gbk2Utf() throws UnsupportedEncodingException {  
  2.     String gbk = "我來了";  
  3.     char[] c = gbk.toCharArray();  
  4.     byte[] fullByte = new byte[3*c.length];  
  5.     for (int i=0; i<c.length; i++) {  
  6.         String binary = Integer.toBinaryString(c[i]);  
  7.         StringBuffer sb = new StringBuffer();  
  8.         int len = 16 - binary.length();  
  9.         //前面補零  
  10.         for(int j=0; j<len; j++){  
  11.                 sb.append("0");  
  12.             }  
  13.         sb.append(binary);  
  14.         //增加位,達到到24位3個字節  
  15.         sb.insert(0"1110");  
  16.             sb.insert(8"10");  
  17.             sb.insert(16"10");  
  18.             fullByte[i*3] = Integer.valueOf(sb.substring(08), 2).byteValue();//二進制字符串創建整型  
  19.             fullByte[i*3+1] = Integer.valueOf(sb.substring(816), 2).byteValue();  
  20.             fullByte[i*3+2] = Integer.valueOf(sb.substring(1624), 2).byteValue();  
  21.     }  
  22.     //模擬UTF-8編碼的網站顯示  
  23.     System.out.println(new String(fullByte,"UTF-8"));  
  24. }  

 

現在我們來找出最後一個字節是63,而不是-122的原因。

Java代碼  收藏代碼
  1. public static void analyze2() throws UnsupportedEncodingException {  
  2.     String gbk = "我來了";  
  3.     byte[] utfBytes = gbk.getBytes("UTF-8");//得到9個字節  
  4.     String utf8 = new String(utfBytes);//問題就出在這  
  5.     System.out.print(utf8);  
  6. }  
  7. /* 
  8. 鎴戞潵浜? 
  9. */  

 因爲文件是GBK編碼,new String(utfBytes)默認就是new String(utfBytes,"GBK")。它會2個字節2個字節地轉換成字符,當字節是奇數時最後1個字節轉字符就會計算錯誤,然後直接賦予最後這個字符爲?,對應ASCII代碼就是63。

 

解決問題
保證字節正確纔是硬道理。當調用getBytes("UTF-8")轉換成字節數組後,創建ISO-8859-1編碼的字符串,ISO-8859-1編碼是一個字節對應一個字符,因此不會使最後一個字節錯誤。

Java代碼  收藏代碼
  1. public static void correctEncode() throws UnsupportedEncodingException {  
  2.     String gbk = "我來了";  
  3.     String iso = new String(gbk.getBytes("UTF-8"),"ISO-8859-1");  
  4.     for (byte b : iso.getBytes("ISO-8859-1")) {  
  5.         System.out.print(b + " ");  
  6.     }  
  7.     System.out.println();  
  8.   
  9.     //模擬UTF-8編碼的網站顯示  
  10.     System.out.println(new String(iso.getBytes("ISO-8859-1"),"UTF-8"));  
  11. }  
  12. /* 
  13. -26 -120 -111 -26 -99 -91 -28 -70 -122  
  14. 我來了 
  15. */  

發佈了35 篇原創文章 · 獲贊 9 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章