編碼的那點事兒

什麼是編碼?


對於普通人來講,編碼老是與一些祕密的東西相關聯(加密與解密);對於程序員們來講,編碼大多數是指一種用來在機器與人之間傳遞信息的方式.html

但從廣義上來說,編碼是從一種信息格式轉換爲另外一種信息格式的過程,解碼則是編碼的逆向過程.接下來舉幾個使用到編碼的例子: java

  • 當咱們要把想表達的意思經過一種語言表達出來,其實就是在腦海中對信息進行了一次編碼,而對方若是也懂得這門語言,那麼就能夠用這門語言的解碼方法(語法規則)來得到信息(平常的說話交流其實就是在編碼與解碼).git

  • 程序員寫程序時,其實就是在將本身的想法經過計算機語言進行編碼,而編譯器則經過生成抽象語法樹,詞義分析等操做進行解碼,最終交給計算機執行程序(編譯器產生的解碼結果並非最終結果,通常爲彙編語言,但彙編語言只是CPU指令集的助記符,還須要再進行解碼).程序員

  • 計算機只有兩種狀態(0和1),要想存儲和傳輸多媒體信息,就須要用到編碼和解碼.
  • 對數據進行壓縮,其本質就是以減小自身佔用的空間爲前提進行從新編碼.

瞭解了編碼的含義,咱們接下來重點探究Java中的字符編碼.github

本文做者爲: SylvanasSun.轉載請務必將下面這段話置於文章開頭處(保留超連接).
本文首發自SylvanasSun Blog,原文連接: sylvanassun.github.io/2017/08/20/…web

常見的字符集


字符集就是字符與二進制的映射表,每個字符集都有本身的編碼規則,每一個字符所佔用的字節也不一樣(支持的字符越多每一個字符佔用的字節也就越多).spring

  • ASCII : 美國信息交換標準碼(American Standard Code for Information Interchange).學過計算機的都知道大名鼎鼎的ASCII碼,它是基於拉丁字母的字符集,總共記有128個字符,主要目的是顯示英語.其中每一個字符佔用一個字節(只用到了低7位).編程

  • ISO-8859-1 : 它是由國際標準化組織(International Standardization Organization)在ASCII基礎上制定的8位字符集(仍然是單字節編碼).它在ASCII空置的0xA0-0xFF範圍內加入了96個字母與符號,支持了歐洲部分國家的語言.數組

  • GBK : 若是咱們想要讓電腦上顯示漢字就必需要有支持漢字的字符集,GBK就是這樣一個支持漢字的字符集,全稱爲<<漢字內碼擴展規範>>,它的編碼方式分爲單字節與雙字節: 00–7F範圍內是第一個字節,與ASCII保持一致,以後的雙字節中,前一字節是雙字節的第一位(範圍在81–FE,不包含80FF),第二字節的一部分在40–7E,其餘部分在80–FE.(這裏再也不介紹GB2313GB18030,它們都是互相兼容的.)瀏覽器

  • UTF-16 : UTF-16Unicode(統一碼,一種以支持世界上多國語言爲目的的通用字符集)的一種實現方式,它把Unicode的抽象碼位映射爲2~4個字節來表示,UTF-16是變長編碼(UTF-32是真正的定長編碼),但在最開始之前UTF-16是用來配合UCS-2(UTF-16的子集,它是定長編碼,用2個字節表示全部Unicode字符)使用的,主要緣由仍是由於當時Unicode只有不到65536個字符,2個字節就足以應對一切了.後來,Unicode支持的字符不斷膨脹,2個字節已經不夠用了,致使一些只支持UCS-2當作內碼的產品很尷尬(Java就是其中之一).

  • UTF-8 : UTF-8也是基於Unicode的變長編碼表,它使用1~6個字節來爲每一個字符進行編碼(RFC 3629UTF-8進行了從新規範,只能使用原來Unicode定義的區域,U+0000~U+10FFFF,也就是說最多隻有4個字節),UTF-8徹底兼容ASCII,它的編碼規則以下:

    • U+0000~U+007F範圍內,只須要一個字節(也就是ASCII字符集中的字符).

    • U+0080~U+07FF範圍內,須要兩個字節(希臘文、阿拉伯文、希伯來文等).

    • U+0800~U+FFFF範圍內,須要三個字節(亞洲漢字等).

    • 其餘的字符使用四個字節.

Java中字符的編解碼


Java提供了Charset類來完成對字符的編碼與解碼,主要使用如下函數:

  • public static Charset forName(String charsetName) : 這是一個靜態工廠函數,它根據傳入的字符集名稱來返回對應字符集的Charset類.
  • public final ByteBuffer encode(CharBuffer cb) / public final ByteBuffer encode(String str) : 編碼函數,它將傳入的字符串或者字符序列進行編碼,返回的ByteBuffer是一個字節緩衝區.
  • public final CharBuffer decode(ByteBuffer bb) : 解碼函數,將傳入的字節序列解碼爲字符序列.

示例代碼


private static final String text = "Hello,編碼!";

    private static final Charset ASCII = Charset.forName("ASCII");

    private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1");

    private static final Charset GBK = Charset.forName("GBK");

    private static final Charset UTF_16 = Charset.forName("UTF-16");

    private static final Charset UTF_8 = Charset.forName("UTF-8");

    private static void encodeAndPrint(Charset charset) {
        System.out.println(charset.name() + ": ");
        printHex(text.toCharArray(), charset);
        System.out.println("----------------------------------");
    }

    private static void printHex(char[] chars, Charset charset) {
        System.out.println("ForEach: ");
        ByteBuffer byteBuffer;
        byte[] bytes;
        if (chars != null) {
            for (char c : chars) {
                System.out.print("char: " + Integer.toHexString(c) + " ");
                // 打印出字符編碼後對應的字節
                byteBuffer = charset.encode(String.valueOf(c));
                bytes = byteBuffer.array();
                System.out.print("byte: ");
                if (bytes != null) {
                    for (byte b : bytes)
                        System.out.print(Integer.toHexString(b & 0xFF) + " ");
                }
                System.out.println();
            }
        }
        System.out.println();
    }複製代碼

有的讀者可能會對以上代碼中的b & 0xFF產生疑惑,這是爲了解決符號擴展問題.在Java中,若是一個窄類型強轉爲一個寬類型時,會對多出來的空位進行符號擴展(若是符號位爲1,就補1,爲0則補0).只有char類型除外,char是沒有符號位的,因此它永遠都是補0.

代碼中調用了函數Integer.toHexString(),變量b在運算以前就已經被強轉爲了int類型,爲了讓數值不受到破壞,咱們讓b0xFF進行了與運算,0xFF是一個低八位都爲1的值(其餘位都爲0),而byte的有效範圍只在低八位,因此結果爲前24位(除符號位)都變爲了0,低八位保留了原有的值.

若是不作這項操做,那麼b又剛好是個負數的話,那這個強轉後的int的前24位都會變爲1,這個結果顯然已經破壞了原有的值.

IO中的字符編碼


ReaderWriterJava中負責字符輸入與輸出的抽象基類,它們的子類實現了在各類場景中的字符輸入輸出功能.

在使用ReaderWriter進行IO操做時,須要指定字符集,若是不顯式指定的話會默認使用當前環境的字符集,但我仍是推薦顯式指定一致的字符集,這樣纔不會出現亂碼問題(ReaderWriter指定的字符集不一致或更改了環境致使字符集不一致等).

public static void writeChar(String content, String filename, String charset) {
        OutputStreamWriter writer = null;

        try {
            FileOutputStream outputStream = new FileOutputStream(filename);
            writer = new OutputStreamWriter(outputStream, charset);
            writer.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (writer != null)
                    writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static String readChar(String filename, String charset) {
        InputStreamReader reader = null;
        StringBuilder sb = null;

        try {
            FileInputStream inputStream = new FileInputStream(filename);
            reader = new InputStreamReader(inputStream, charset);
            char[] buf = new char[64];
            int count = 0;
            sb = new StringBuilder();
            while ((count = reader.read(buf)) != -1)
                sb.append(buf, 0, count);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (reader != null)
                    reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return sb.toString();
    }複製代碼

Web中的字符編碼


Web開發中,亂碼也是常常存在的一個問題,主要體如今請求的參數和返回的響應結果,最頭疼的是不一樣的瀏覽器的默認編碼甚至還不一致.

JavaHttp的請求與響應抽象出了RequestResponse兩個對象,只要保持請求與響應的編碼一致就能避免亂碼問題.

Request提供了setCharacterEncoding(String encode)函數來改變請求體的編碼,通常經過寫一個過濾器來統一對全部請求設置編碼.

request.setCharacterEncoding("UTF-8");複製代碼

Response提供了setCharacterEncoding(String encode)setHeader(String name,String value)兩個函數,它們均可以設置響應的編碼.

response.setCharacterEncoding("UTF-8");
// 設置響應頭的編碼信息,同時也告知了瀏覽器該如何解碼
response.setHeader("Content-Type","text/html;charset=UTF-8");複製代碼

還有一種更簡便的方式,直接使用Spring提供的CharacterEncodingFilter,該過濾器就是用來統一編碼的.

<filter>
    <filter-name>charsetFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
   <filter-name>charsetFilter</filter-name>
   <url-pattern>*</url-pattern>
</filter-mapping>複製代碼

CharacterEncodingFilter的實現以下:

public class CharacterEncodingFilter extends OncePerRequestFilter {
    private String encoding;
    private boolean forceEncoding = false;

    public CharacterEncodingFilter() {
    }

    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

    public void setForceEncoding(boolean forceEncoding) {
        this.forceEncoding = forceEncoding;
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if(this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) {
            request.setCharacterEncoding(this.encoding);
            if(this.forceEncoding) {
                response.setCharacterEncoding(this.encoding);
            }
        }

        filterChain.doFilter(request, response);
    }
}複製代碼

爲何Char在Java中佔用兩個字節?


衆所周知,在Java中一個char類型佔用兩個字節,那麼這是爲何呢?這是由於Java使用了UTF-16看成內碼.

內碼(Internal Encoding)就是程序內部所使用的編碼,主要在於編程語言實現其charString類型在內存中使用的內部編碼.與之相對的就是外碼(External Encoding),它是程序與外部交互時使用的字符編碼.

值得一提的是,當初UTF-16是配合UCS-2使用的,後來Unicode支持的字符不斷增多,UTF-16也再也不只看成一個定長的2字節編碼使用了,也就是說,Java中的一個char其實並不必定能表明一個完整的UTF-16字符.

String.getBytes()能夠將該String的內碼轉換爲指定的外碼並返回這個編完碼的字節數組(無參數版使用當前平臺的默認編碼).

public static void main(String[] args) throws UnsupportedEncodingException {
        String text = "碼";
        byte[] bytes = text.getBytes("UTF-8"); 
        System.out.println(bytes.length); // 輸出3
    }複製代碼

Java還規定charString類型的序列化是使用UTF-8看成外碼的,Java中的Class文件中的字符串常量與符號名也都規定使用UTF-8.這種設計是爲了平衡運行時的時間效率與外部存儲的空間效率所作的取捨.

SUN JDK6中,有一條命令-XX:+UseCompressedString.該命令可讓String內部存儲字符內容可能用byte[]也可能用char[]: 當整個字符串全部字符處於ASCII字符集範圍內時,就使用byte[](使用了ASCII編碼)來存儲,若是有任一字符超過了ASCII的範圍,就退回到使用char[](UTF-16編碼)來存儲.可是這個功能實現的並不理想,因此沒有包含在Open JDK6/Open JDK7/Oracle JDK7等後續版本中.

JavaScript也使用了UTF-16做爲內碼,其實現也普遍應用了CompressedString的思想,主流的JavaScript引擎中都會盡量使用ASCII內碼的字符串,不過這些細節都是對外隱藏的..

參考文獻


相關文章
相關標籤/搜索