編碼的那點(diǎn)事兒

什么是編碼?


對(duì)于普通人來(lái)說(shuō),編碼總是與一些秘密的東西相關(guān)聯(lián)(加密與解密);對(duì)于程序員們來(lái)說(shuō),編碼大多數(shù)是指一種用來(lái)在機(jī)器與人之間傳遞信息的方式.

但從廣義上來(lái)講,編碼是從一種信息格式轉(zhuǎn)換為另一種信息格式的過(guò)程,解碼則是編碼的逆向過(guò)程.接下來(lái)舉幾個(gè)使用到編碼的例子:

  • 當(dāng)我們要把想表達(dá)的意思通過(guò)一種語(yǔ)言表達(dá)出來(lái),其實(shí)就是在腦海中對(duì)信息進(jìn)行了一次編碼,而對(duì)方如果也懂得這門(mén)語(yǔ)言,那么就可以用這門(mén)語(yǔ)言的解碼方法(語(yǔ)法規(guī)則)來(lái)獲得信息(日常的說(shuō)話(huà)交流其實(shí)就是在編碼與解碼).

  • 程序員寫(xiě)程序時(shí),其實(shí)就是在將自己的想法通過(guò)計(jì)算機(jī)語(yǔ)言進(jìn)行編碼,而編譯器則通過(guò)生成抽象語(yǔ)法樹(shù),詞義分析等操作進(jìn)行解碼,最終交給計(jì)算機(jī)執(zhí)行程序(編譯器產(chǎn)生的解碼結(jié)果并不是最終結(jié)果,一般為匯編語(yǔ)言,但匯編語(yǔ)言只是CPU指令集的助記符,還需要再進(jìn)行解碼).

  • 計(jì)算機(jī)只有兩種狀態(tài)(0和1),要想存儲(chǔ)和傳輸多媒體信息,就需要用到編碼和解碼.
  • 對(duì)數(shù)據(jù)進(jìn)行壓縮,其本質(zhì)就是以減少自身占用的空間為前提進(jìn)行重新編碼.

了解了編碼的含義,我們接下來(lái)重點(diǎn)探究Java中的字符編碼.

本文作者為: SylvanasSun.轉(zhuǎn)載請(qǐng)務(wù)必將下面這段話(huà)置于文章開(kāi)頭處(保留超鏈接).
本文首發(fā)自SylvanasSun Blog,原文鏈接: https://sylvanassun.github.io/2017/08/20/2017-08-20-Encode/

常見(jiàn)的字符集


字符集就是字符與二進(jìn)制的映射表,每一個(gè)字符集都有自己的編碼規(guī)則,每個(gè)字符所占用的字節(jié)也不同(支持的字符越多每個(gè)字符占用的字節(jié)也就越多).

  • ASCII : 美國(guó)信息交換標(biāo)準(zhǔn)碼(American Standard Code for Information Interchange).學(xué)過(guò)計(jì)算機(jī)的都知道大名鼎鼎的ASCII碼,它是基于拉丁字母的字符集,總共記有128個(gè)字符,主要目的是顯示英語(yǔ).其中每個(gè)字符占用一個(gè)字節(jié)(只用到了低7位).

  • ISO-8859-1 : 它是由國(guó)際標(biāo)準(zhǔn)化組織(International Standardization Organization)在ASCII基礎(chǔ)上制定的8位字符集(仍然是單字節(jié)編碼).它在ASCII空置的0xA0-0xFF范圍內(nèi)加入了96個(gè)字母與符號(hào),支持了歐洲部分國(guó)家的語(yǔ)言.

  • GBK : 如果我們想要讓電腦上顯示漢字就必須要有支持漢字的字符集,GBK就是這樣一個(gè)支持漢字的字符集,全稱(chēng)為<<漢字內(nèi)碼擴(kuò)展規(guī)范>>,它的編碼方式分為單字節(jié)與雙字節(jié): 00–7F范圍內(nèi)是第一個(gè)字節(jié),與ASCII保持一致,之后的雙字節(jié)中,前一字節(jié)是雙字節(jié)的第一位(范圍在81–FE,不包含80FF),第二字節(jié)的一部分在40–7E,其他部分在80–FE.(這里不再介紹GB2313GB18030,它們都是互相兼容的.)

  • UTF-16 : UTF-16Unicode(統(tǒng)一碼,一種以支持世界上多國(guó)語(yǔ)言為目的的通用字符集)的一種實(shí)現(xiàn)方式,它把Unicode的抽象碼位映射為2~4個(gè)字節(jié)來(lái)表示,UTF-16是變長(zhǎng)編碼(UTF-32是真正的定長(zhǎng)編碼),但在最開(kāi)始以前UTF-16是用來(lái)配合UCS-2(UTF-16的子集,它是定長(zhǎng)編碼,用2個(gè)字節(jié)表示所有Unicode字符)使用的,主要原因還是因?yàn)楫?dāng)時(shí)Unicode只有不到65536個(gè)字符,2個(gè)字節(jié)就足以應(yīng)對(duì)一切了.后來(lái),Unicode支持的字符不斷膨脹,2個(gè)字節(jié)已經(jīng)不夠用了,導(dǎo)致一些只支持UCS-2當(dāng)做內(nèi)碼的產(chǎn)品很尷尬(Java就是其中之一).

  • UTF-8 : UTF-8也是基于Unicode的變長(zhǎng)編碼表,它使用1~6個(gè)字節(jié)來(lái)為每個(gè)字符進(jìn)行編碼(RFC 3629對(duì)UTF-8進(jìn)行了重新規(guī)范,只能使用原來(lái)Unicode定義的區(qū)域,U+0000~U+10FFFF,也就是說(shuō)最多只有4個(gè)字節(jié)),UTF-8完全兼容ASCII,它的編碼規(guī)則如下:

    • U+0000~U+007F范圍內(nèi),只需要一個(gè)字節(jié)(也就是ASCII字符集中的字符).

    • U+0080~U+07FF范圍內(nèi),需要兩個(gè)字節(jié)(希臘文、阿拉伯文、希伯來(lái)文等).

    • U+0800~U+FFFF范圍內(nèi),需要三個(gè)字節(jié)(亞洲漢字等).

    • 其他的字符使用四個(gè)字節(jié).

Java中字符的編解碼


Java提供了Charset類(lèi)來(lái)完成對(duì)字符的編碼與解碼,主要使用以下函數(shù):

  • public static Charset forName(String charsetName) : 這是一個(gè)靜態(tài)工廠(chǎng)函數(shù),它根據(jù)傳入的字符集名稱(chēng)來(lái)返回對(duì)應(yīng)字符集的Charset類(lèi).
  • public final ByteBuffer encode(CharBuffer cb) / public final ByteBuffer encode(String str) : 編碼函數(shù),它將傳入的字符串或者字符序列進(jìn)行編碼,返回的ByteBuffer是一個(gè)字節(jié)緩沖區(qū).
  • public final CharBuffer decode(ByteBuffer bb) : 解碼函數(shù),將傳入的字節(jié)序列解碼為字符序列.

示例代碼


    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) + " ");
                // 打印出字符編碼后對(duì)應(yīng)的字節(jié)
                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();
    }

有的讀者可能會(huì)對(duì)以上代碼中的b & 0xFF產(chǎn)生疑惑,這是為了解決符號(hào)擴(kuò)展問(wèn)題.在Java中,如果一個(gè)窄類(lèi)型強(qiáng)轉(zhuǎn)為一個(gè)寬類(lèi)型時(shí),會(huì)對(duì)多出來(lái)的空位進(jìn)行符號(hào)擴(kuò)展(如果符號(hào)位為1,就補(bǔ)1,為0則補(bǔ)0).只有char類(lèi)型除外,char是沒(méi)有符號(hào)位的,所以它永遠(yuǎn)都是補(bǔ)0.

代碼中調(diào)用了函數(shù)Integer.toHexString(),變量b在運(yùn)算之前就已經(jīng)被強(qiáng)轉(zhuǎn)為了int類(lèi)型,為了讓數(shù)值不受到破壞,我們讓b對(duì)0xFF進(jìn)行了與運(yùn)算,0xFF是一個(gè)低八位都為1的值(其他位都為0),而byte的有效范圍只在低八位,所以結(jié)果為前24位(除符號(hào)位)都變?yōu)榱?,低八位保留了原有的值.

如果不做這項(xiàng)操作,那么b又恰好是個(gè)負(fù)數(shù)的話(huà),那這個(gè)強(qiáng)轉(zhuǎn)后的int的前24位都會(huì)變?yōu)?,這個(gè)結(jié)果顯然已經(jīng)破壞了原有的值.

IO中的字符編碼


ReaderWriterJava中負(fù)責(zé)字符輸入與輸出的抽象基類(lèi),它們的子類(lèi)實(shí)現(xiàn)了在各種場(chǎng)景中的字符輸入輸出功能.

在使用ReaderWriter進(jìn)行IO操作時(shí),需要指定字符集,如果不顯式指定的話(huà)會(huì)默認(rèn)使用當(dāng)前環(huán)境的字符集,但我還是推薦顯式指定一致的字符集,這樣才不會(huì)出現(xiàn)亂碼問(wèn)題(ReaderWriter指定的字符集不一致或更改了環(huán)境導(dǎo)致字符集不一致等).

    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開(kāi)發(fā)中,亂碼也是經(jīng)常存在的一個(gè)問(wèn)題,主要體現(xiàn)在請(qǐng)求的參數(shù)和返回的響應(yīng)結(jié)果,最頭疼的是不同的瀏覽器的默認(rèn)編碼甚至還不一致.

JavaHttp的請(qǐng)求與響應(yīng)抽象出了RequestResponse兩個(gè)對(duì)象,只要保持請(qǐng)求與響應(yīng)的編碼一致就能避免亂碼問(wèn)題.

Request提供了setCharacterEncoding(String encode)函數(shù)來(lái)改變請(qǐng)求體的編碼,一般通過(guò)寫(xiě)一個(gè)過(guò)濾器來(lái)統(tǒng)一對(duì)所有請(qǐng)求設(shè)置編碼.

request.setCharacterEncoding("UTF-8");

Response提供了setCharacterEncoding(String encode)setHeader(String name,String value)兩個(gè)函數(shù),它們都可以設(shè)置響應(yīng)的編碼.

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

還有一種更簡(jiǎn)便的方式,直接使用Spring提供的CharacterEncodingFilter,該過(guò)濾器就是用來(lái)統(tǒng)一編碼的.

<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的實(shí)現(xiàn)如下:

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中占用兩個(gè)字節(jié)?


眾所周知,在Java中一個(gè)char類(lèi)型占用兩個(gè)字節(jié),那么這是為什么呢?這是因?yàn)?code>Java使用了UTF-16當(dāng)作內(nèi)碼.

內(nèi)碼(Internal Encoding)就是程序內(nèi)部所使用的編碼,主要在于編程語(yǔ)言實(shí)現(xiàn)其charString類(lèi)型在內(nèi)存中使用的內(nèi)部編碼.與之相對(duì)的就是外碼(External Encoding),它是程序與外部交互時(shí)使用的字符編碼.

值得一提的是,當(dāng)初UTF-16是配合UCS-2使用的,后來(lái)Unicode支持的字符不斷增多,UTF-16也不再只當(dāng)作一個(gè)定長(zhǎng)的2字節(jié)編碼使用了,也就是說(shuō),Java中的一個(gè)char其實(shí)并不一定能代表一個(gè)完整的UTF-16字符.

String.getBytes()可以將該String的內(nèi)碼轉(zhuǎn)換為指定的外碼并返回這個(gè)編完碼的字節(jié)數(shù)組(無(wú)參數(shù)版使用當(dāng)前平臺(tái)的默認(rèn)編碼).

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

Java還規(guī)定charString類(lèi)型的序列化是使用UTF-8當(dāng)作外碼的,Java中的Class文件中的字符串常量與符號(hào)名也都規(guī)定使用UTF-8.這種設(shè)計(jì)是為了平衡運(yùn)行時(shí)的時(shí)間效率與外部存儲(chǔ)的空間效率所做的取舍.

SUN JDK6中,有一條命令-XX:+UseCompressedString.該命令可以讓String內(nèi)部存儲(chǔ)字符內(nèi)容可能用byte[]也可能用char[]: 當(dāng)整個(gè)字符串所有字符處于ASCII字符集范圍內(nèi)時(shí),就使用byte[](使用了ASCII編碼)來(lái)存儲(chǔ),如果有任一字符超過(guò)了ASCII的范圍,就退回到使用char[](UTF-16編碼)來(lái)存儲(chǔ).但是這個(gè)功能實(shí)現(xiàn)的并不理想,所以沒(méi)有包含在Open JDK6/Open JDK7/Oracle JDK7等后續(xù)版本中.

JavaScript也使用了UTF-16作為內(nèi)碼,其實(shí)現(xiàn)也廣泛應(yīng)用了CompressedString的思想,主流的JavaScript引擎中都會(huì)盡可能使用ASCII內(nèi)碼的字符串,不過(guò)這些細(xì)節(jié)都是對(duì)外隱藏的..

參考文獻(xiàn)


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 編碼問(wèn)題一直困擾著開(kāi)發(fā)人員,尤其在 Java 中更加明顯,因?yàn)?Java 是跨平臺(tái)語(yǔ)言,不同平臺(tái)之間編碼之間的切換...
    x360閱讀 2,580評(píng)論 1 20
  • 為什么要編碼 不知道大家有沒(méi)有想過(guò)一個(gè)問(wèn)題,那就是為什么要編碼?我們能不能不編碼?要回答這個(gè)問(wèn)題必須要回到計(jì)算機(jī)是...
    艾小天兒閱讀 17,877評(píng)論 0 2
  • 可以看我的博客 lmwen.top 或者訂閱我的公眾號(hào) 簡(jiǎn)介有稍微接觸python的人就會(huì)知道,python中...
    ayuLiao閱讀 3,395評(píng)論 1 5
  • 那些年我還小,不管我做什么事,我都想要得到父母的贊美,哪怕是一個(gè)淺淺的微笑,也是最大的鼓舞,可是,當(dāng)我發(fā)現(xiàn)我...
    楊情拾光閱讀 342評(píng)論 0 1
  • 最近很喜歡看的一部番吖~~~
    弋一知曉春閱讀 357評(píng)論 7 2

友情鏈接更多精彩內(nèi)容