背景
從一個問題說起,前幾天在和一家公司做項目對接時,我方公司提供給對方的是返回的code碼作爲成功還是失敗校驗,對方公司因爲使用了我方返回的msg作爲組合校驗,而返回msg出現亂碼,導致對方公司作字符串匹配時失敗,以爲我方返回的是失敗。這時,對方公司截圖發給我方要求我方檢查編碼。我雖不才,但是我方使用的編碼我還是可以保證的,我敢大聲的說不是我方的問題。爲此開啓了一條甩鍋之路(不對啊,明明不是我的鍋,爲什麼要叫甩鍋)。
這時,我開始找證據,首先我得搞清楚那句亂碼正常應該是什麼信息,我看到該請求在執行數據查詢時拋了一個RuntimeException,這個會交給全局異常處理器處理,然後找到了本來該返回的信息“內部系統錯誤,請聯繫管理員”這一句,總共13箇中文字符,這時我在看了一下對方截圖的亂碼如下圖所示,我數了一下總共39個亂碼字符,哈哈,這個字數符和我方UTF-8的編碼編碼13箇中文字符的字節數剛剛吻合,這個時候已經可以甩掉這個鍋,但是我決定幫對方找到他們錯誤使用的編碼,這時浮現在我腦海中單字節編碼集ISO-8859-1和ASCII。
接下來我要幫對方找到問題的原因,寫了一段測試代碼
public class EncodeTest {
public static void main(String[] args) {
String str = "系統內部錯誤,請聯繫管理員";
try {
byte[] bytes = str.getBytes("UTF-8");
String newStr = new String(bytes, "iso-8859-1");
//String newStr = new String(bytes, "ASCII");
System.out.println(newStr);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
iso-8859-1的輸出
系统å†éƒ¨é”™è¯¯ï¼Œè¯·è”系管ç†å‘˜
ASCII的輸出
���������������������������������������
完美,這下可以確定對方在解碼我方返回結果時採用的是什麼編碼了,就是ASCII碼。眼尖的朋友可能看到了爲什麼圖片裏還有其他正常的中文,如果有這個疑問,看完後邊的解析後應該能夠明白。
編碼集
單字節編碼
-
ASCII
ASCII是現今最通用的單字節編碼系統,使用7位二進制數來表示所有的字母、數字、標點符號及一些特殊控制字符,作爲美國編碼標準來使用。ASCII定義了128個字符,包括33個不可打印的控制字符(non-printing control characters)和95個可打印的字符。
-
ISO-8859-1
ISO-8859-1是單字節編碼,又稱Latin-1,用8爲編碼,向下兼容ASCII,是許多歐洲國家使用的編碼標準。其編碼範圍是0x00-0xFF,0x00-0x7F之間完全和ASCII一致,0x80-0x9F之間是控制字符,0xA0-0xFF之間是文字符號。
多字節編碼
單字節編碼在使用英文的國家可以滿足需求,可是對於其他國家的文字則不能用單字節編碼,比如我們使用的中文。因爲ISO-8859-1使用8位編碼,最多也只能表示256個字符,而世界上其他國家的文字,單是我們使用的中文就遠遠不止256。
-
GB2312
gb2312使用兩個字節(或一個字節,半角字符)編碼,分爲高位和低位,規定當字節值小於127時和ASCII碼相同,如果兩個大於127的字節連着時,就表示中文字符,此外這些編碼裏,還把數學符號、羅馬希臘的字母、日文的假名們都編進去了,連在 ASCII 裏本來就有的數字、標點、字母都統統重新編碼,這就是常說的”全角”字符,而原來在127號以下的那些就叫”半角”字符了。
-
GBK
即使GB2312可以編碼7000多個字符,但是還是不夠,於是又進一步規定兩個字節中的低位不一定要大於127,只要高位大於127就表示一箇中文字符。通過這樣擴展就增加了20000多箇中文字符,其中包括繁體字。
-
GB18030
擴展了GBK,增加了少數名族字符。
-
UNICODE
由於各個國家都這樣自己搞了一套編碼,導致非常不通用,ISO解決了這個問題,廢棄了其他國家或地區的編碼,制定了一套國際通用的編碼,該編碼強制用兩個字節來表示任何字符,包括ASCII中的半角字符也是兩字節編碼,UNICODE可以表示65535個不同字符。
-
UTF
由於網絡的興起,UNICODE由於必須由兩個字節表示,在網絡傳輸中那些半角字符會浪費帶寬,所以UNICODE在網絡中如何傳輸就成爲一個問題,這時出現各種UTF標準,UTF-8就是每次8位傳輸數據,UTF-16則一次傳輸16位。UTF-8編碼從1-6字節,編碼常用漢字用3個字節。
例子
現在舉個例子,我們以“你好啊,Ikan!”爲例子,分別看一下將它編碼成ISO-8859-1、GBK、UTF-8,所佔的字節數和字節數組的值是否與上一小節描述相符。
-
ISO-8859-1編碼解碼
從圖中看到,用ISO編碼中文,因爲找不到對應的編碼,全部被編碼爲了63,所以就出現大家很熟悉的亂碼。
-
GBK編碼解碼
GBK用兩個字節編碼中文字符,用一個字節編碼ASCII字符。
-
UTF-8編碼解碼
UTF-8用3個字節編碼中文字符,用一個字節編碼ASCII字符。具體是怎麼表示的呢?UTF-8規定,字節以0開頭表示後面7位表示一個ASCII字符,如果以10開頭表示這個字節後6位表示字符的部分內容,如果以110\1110\11110…表示這是一個起始字符有多少個1就表示這個字符有多少個字節。
-
亂碼產生的原因
通過上面的分析,可以知道,亂碼產生的原因
-
用ASCII或ISO-8859-1編碼中文,導致信息丟失,無法還原。
-
編碼和解碼用的不是同一種編碼,比如用GBK解UTF-8的編碼
從上圖看到中文字符串的前四個字符編碼爲12個字節,而GBK用2個字節編碼中文,所以產生了6箇中文字符,因爲編碼不一致,導致不能得到正確的中文。由此得到一個規律,當原始字符串中有中文時,gbk解utf-8編碼的字符將得到比原始字符串多的亂碼字符串,同理iso-8859-1和ASCII碼。
-
Java中的編碼
Java內編碼
Java的內編碼指JVM運行時是如何表示字符,Java的內編碼用UTF-16,UTF-16即用兩個字節來編碼字符。大家可能對Java的編碼系統有疑惑,或者經常Java運行時亂碼搞不清楚,看一張圖,來理一下Java從編輯java文件到編譯然後在到JVM加載運行到傳輸過程中字符的表示方式以及轉換過程。
上圖中我們看到不管你編輯.java文件時用的是什麼編碼,這就是我們可能經常用ide編輯代碼時project是UTF-8這個時候ide在編譯時是按UTF-8來編碼,如果這個時候不知你從哪裏複製過來一個.java文件,這個文件是GBK編碼的,剛好這個文件裏又有中文,這個時候如果編譯,那麼這個文件中的中文在程序運行產生結果傳輸時就會變亂碼。在編譯時都會被轉換爲UTF-8表示的.class文件(注意:在編譯時如果沒有指定.java文件編碼,會採用系統本地編碼格式),然後被加載到內存中時,我們知道在運行時,String內部其實是char數組,而我們知道一個char由兩個byte組成,所以在運行時字符是UTF-16表示。至於如果傳輸程序結果,這個可以有多種編碼方式,看程序如何指定。這個涉及到字符集和編碼解碼,接下來詳細介紹這些部分。
CharsetEncoder(編碼器)
-
CharsetEncoder構造方法
protected CharsetEncoder(Charset cs, float averageBytesPerChar, float maxBytesPerChar) protected CharsetEncoder(Charset cs, float averageBytesPerChar, float maxBytesPerChar, byte[] replacement)
通過兩個構造函數的修飾符,可以知道CharsetEncoder的構造函數是受保護的,設計者不希望用戶直接構造該對象,如果用戶要使用該對象那麼就要通過Charset的newEncoder方法獲取到CharsetEncoder對象,或者是通過繼承CharsetEncoder來構造。
-
CharsetEncoder方法
CharsetEncoder方法較多,感興趣的可以從這裏去查看詳細的介紹。
CharsetDecoder(解碼器)
CharsetDecoder和CharsetEncoder類似,不囉嗦了,感興趣的可以通過這裏獲得詳細介紹。
CharSet(字符集)
Java支持的字符集很廣泛,CharSet是所有字符集的父類。這裏列幾個常用的字符集和CharSet的繼承關係圖
Charset是一個抽象類,本身是不能實例化的。
-
Charset方法
Set<String> aliases() static SortedMap<String,Charset> availableCharsets() boolean canEncode() //和傳入的字符集比較 int compareTo(Charset that) // abstract boolean contains(Charset cs) //重要方法,傳入byte緩衝以該字符集編碼方式解碼 CharBuffer decode(ByteBuffer bb) static Charset defaultCharset() String displayName() String displayName(Locale locale) //重要方法,傳入字符串以該字符集編碼方式編碼 ByteBuffer encode(String str) //重要方法,傳入字符緩衝以該字符集編碼方式編碼 ByteBuffer encode(CharBuffer cb) boolean equals(Object ob) static Charset forName(String charsetName) int hashCode() boolean isRegistered() static boolean isSupported(String charsetName) String name() //創建一個解碼器 abstract CharsetDecoder newDecoder() //創建一個編碼器 abstract CharsetEncoder newEncoder() String toString()
-
簡單例子
public static void main(String[] args) { Charset charset = Charset.forName("UTF-8"); ByteBuffer byteBuffer = charset.encode("你好啊,Ikan!"); for (int i=0; i<byteBuffer.array().length; i++){ System.out.print(byteBuffer.array()[i]); } System.out.println(); System.out.println(charset.decode(ByteBuffer.wrap(byteBuffer.array()))); }
輸出結果:
-28 -67 -96 -27 -91 -67 -27 -107 -118 -17 -68 -116 73 107 97 110 -17 -68 -127 你好啊,Ikan!
和我們前邊的UTF-8編碼一樣。
文章首發地址:IkanのBolg