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

什么是編碼?


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

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

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

  • 程序員寫程序時(shí),其實(shí)就是在將自己的想法通過計(jì)算機(jī)語(yǔ)言進(jìn)行編碼,而編譯器則通過生成抽象語(yǔ)法樹,詞義分析等操作進(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)行重新編碼.

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

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

常見的字符集


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

  • ASCII : 美國(guó)信息交換標(biāo)準(zhǔn)碼(American Standard Code for Information Interchange).學(xué)過計(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è)支持漢字的字符集,全稱為<<漢字內(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é)來表示,UTF-16是變長(zhǎng)編碼(UTF-32是真正的定長(zhǎng)編碼),但在最開始以前UTF-16是用來配合UCS-2(UTF-16的子集,它是定長(zhǎng)編碼,用2個(gè)字節(jié)表示所有Unicode字符)使用的,主要原因還是因?yàn)楫?dāng)時(shí)Unicode只有不到65536個(gè)字符,2個(gè)字節(jié)就足以應(yīng)對(duì)一切了.后來,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é)來為每個(gè)字符進(jìn)行編碼(RFC 3629對(duì)UTF-8進(jìn)行了重新規(guī)范,只能使用原來Unicode定義的區(qū)域,U+0000~U+10FFFF,也就是說最多只有4個(gè)字節(jié)),UTF-8完全兼容ASCII,它的編碼規(guī)則如下:

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

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

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

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

Java中字符的編解碼


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

  • public static Charset forName(String charsetName) : 這是一個(gè)靜態(tài)工廠函數(shù),它根據(jù)傳入的字符集名稱來返回對(duì)應(yīng)字符集的Charset類.
  • 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ò)展問題.在Java中,如果一個(gè)窄類型強(qiáng)轉(zhuǎn)為一個(gè)寬類型時(shí),會(huì)對(duì)多出來的空位進(jìn)行符號(hào)擴(kuò)展(如果符號(hào)位為1,就補(bǔ)1,為0則補(bǔ)0).只有char類型除外,char是沒有符號(hào)位的,所以它永遠(yuǎn)都是補(bǔ)0.

代碼中調(diào)用了函數(shù)Integer.toHexString(),變量b在運(yùn)算之前就已經(jīng)被強(qiáng)轉(zhuǎn)為了int類型,為了讓數(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ù)的話,那這個(gè)強(qiáng)轉(zhuǎn)后的int的前24位都會(huì)變?yōu)?,這個(gè)結(jié)果顯然已經(jīng)破壞了原有的值.

IO中的字符編碼


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

在使用ReaderWriter進(jìn)行IO操作時(shí),需要指定字符集,如果不顯式指定的話會(huì)默認(rèn)使用當(dāng)前環(huán)境的字符集,但我還是推薦顯式指定一致的字符集,這樣才不會(huì)出現(xià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開發(fā)中,亂碼也是經(jīng)常存在的一個(gè)問題,主要體現(xiàn)在請(qǐng)求的參數(shù)和返回的響應(yīng)結(jié)果,最頭疼的是不同的瀏覽器的默認(rèn)編碼甚至還不一致.

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

Request提供了setCharacterEncoding(String encode)函數(shù)來改變請(qǐng)求體的編碼,一般通過寫一個(gè)過濾器來統(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,該過濾器就是用來統(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類型占用兩個(gè)字節(jié),那么這是為什么呢?這是因?yàn)?code>Java使用了UTF-16當(dāng)作內(nèi)碼.

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

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

String.getBytes()可以將該String的內(nèi)碼轉(zhuǎn)換為指定的外碼并返回這個(gè)編完碼的字節(jié)數(shù)組(無參數(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類型的序列化是使用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編碼)來存儲(chǔ),如果有任一字符超過了ASCII的范圍,就退回到使用char[](UTF-16編碼)來存儲(chǔ).但是這個(gè)功能實(shí)現(xiàn)的并不理想,所以沒有包含在Open JDK6/Open JDK7/Oracle JDK7等后續(xù)版本中.

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

參考文獻(xiàn)


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末驳规,一起剝皮案震驚了整個(gè)濱河市飒责,隨后出現(xiàn)的幾起案子署鸡,更是在濱河造成了極大的恐慌鞍匾,老刑警劉巖瘸恼,帶你破解...
    沈念sama閱讀 222,378評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異似谁,居然都是意外死亡傲绣,警方通過查閱死者的電腦和手機(jī)掠哥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秃诵,“玉大人续搀,你說我怎么就攤上這事〔ぞ唬” “怎么了禁舷?”我有些...
    開封第一講書人閱讀 168,983評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)毅往。 經(jīng)常有香客問我牵咙,道長(zhǎng),這世上最難降的妖魔是什么攀唯? 我笑而不...
    開封第一講書人閱讀 59,938評(píng)論 1 299
  • 正文 為了忘掉前任洁桌,我火速辦了婚禮,結(jié)果婚禮上侯嘀,老公的妹妹穿的比我還像新娘另凌。我一直安慰自己,他們只是感情好戒幔,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評(píng)論 6 398
  • 文/花漫 我一把揭開白布吠谢。 她就那樣靜靜地躺著,像睡著了一般诗茎。 火紅的嫁衣襯著肌膚如雪工坊。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,549評(píng)論 1 312
  • 那天错沃,我揣著相機(jī)與錄音栅组,去河邊找鬼。 笑死枢析,一個(gè)胖子當(dāng)著我的面吹牛玉掸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播醒叁,決...
    沈念sama閱讀 41,063評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼司浪,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了把沼?” 一聲冷哼從身側(cè)響起啊易,我...
    開封第一講書人閱讀 39,991評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎饮睬,沒想到半個(gè)月后租谈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,522評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評(píng)論 3 342
  • 正文 我和宋清朗相戀三年割去,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窟却。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,742評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡呻逆,死狀恐怖夸赫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情咖城,我是刑警寧澤茬腿,帶...
    沈念sama閱讀 36,413評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站宜雀,受9級(jí)特大地震影響切平,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辐董,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評(píng)論 3 335
  • 文/蒙蒙 一揭绑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧郎哭,春花似錦、人聲如沸菇存。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)依鸥。三九已至亥至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贱迟,已是汗流浹背姐扮。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衣吠,地道東北人茶敏。 一個(gè)月前我還...
    沈念sama閱讀 49,159評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像缚俏,于是被迫代替她去往敵國(guó)和親惊搏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

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