一個生產問題引發的Java編碼思考

背景

從一個問題說起,前幾天在和一家公司做項目對接時,我方公司提供給對方的是返回的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就表示這個字符有多少個字節。

  • 亂碼產生的原因

    通過上面的分析,可以知道,亂碼產生的原因

    1. 用ASCII或ISO-8859-1編碼中文,導致信息丟失,無法還原。

    2. 編碼和解碼用的不是同一種編碼,比如用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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章