App下載

總結(jié)歸納Java基礎(chǔ)知識點之字符串編碼的詳細(xì)內(nèi)容

來源: 怪味少女 2021-08-17 14:53:20 瀏覽數(shù) (2189)
反饋

一、為什么要編碼

不知道大家有沒有想過一個問題,那就是為什么要編碼?我們能不能不編碼?要回答這個問題必須要回到計算機是如何表示我們?nèi)祟惸軌蚶斫獾姆柕模@些符號也就是我們?nèi)祟愂褂玫恼Z言。由于人類的語言有太多,因而表示這些語言的符號太多,無法用計算機中一個基本的存儲單元—— byte 來表示,因而必須要經(jīng)過拆分或一些翻譯工作,才能讓計算機能理解。我們可以把計算機能夠理解的語言假定為英語,其它語言要能夠在計算機中使用必須經(jīng)過一次翻譯,把它翻譯成英語。這個翻譯的過程就是編碼。所以可以想象只要不是說英語的國家要能夠使用計算機就必須要經(jīng)過編碼。這看起來有些霸道,但是這就是現(xiàn)狀,這也和我們國家現(xiàn)在在大力推廣漢語一樣,希望其它國家都會說漢語,以后其它的語言都翻譯成漢語,我們可以把計算機中存儲信息的最小單位改成漢字,這樣我們就不存在編碼問題了。

所以總的來說,編碼的原因可以總結(jié)為:

  • 計算機中存儲信息的最小單元是一個字節(jié)即 8 個 bit,所以能表示的字符范圍是 0~255 個
  • 人類要表示的符號太多,無法用一個字節(jié)來完全表示,要解決這個矛盾必須需要一個新的數(shù)據(jù)結(jié)構(gòu) char,從 char 到 byte 必須編碼

二、如何“翻譯”

明白了各種語言需要交流,經(jīng)過翻譯是必要的,那又如何來翻譯呢?計算中提拱了多種翻譯方式,常見的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16 等。它們都可以被看作為字典,它們規(guī)定了轉(zhuǎn)化的規(guī)則,按照這個規(guī)則就可以讓計算機正確的表示我們的字符。目前的編碼格式很多,例如 GB2312、GBK、UTF-8、UTF-16 這幾種格式都可以表示一個漢字,那我們到底選擇哪種編碼格式來存儲漢字呢?這就要考慮到其它因素了,是存儲空間重要還是編碼的效率重要。根據(jù)這些因素來正確選擇編碼格式,下面簡要介紹一下這幾種編碼格式。

  • ASCII 碼

學(xué)過計算機的人都知道 ASCII 碼,總共有 128 個(0-127),用一個字節(jié)的低 7 位表示,0~31 是控制字符如換行回車刪除等;32~126 是打印字符,可以通過鍵盤輸入并且能夠顯示出來。

其中48~57為0到9十個阿拉伯?dāng)?shù)字
65~90為26個大寫英文字母
97~122號為26個小寫英文字母

  • ISO-8859-1

128 個字符顯然是不夠用的,于是 ISO 組織在 ASCII 碼基礎(chǔ)上又制定了一些列標(biāo)準(zhǔn)用來擴展 ASCII 編碼,它們是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵蓋了大多數(shù)西歐語言字符,所有應(yīng)用的最廣泛。ISO-8859-1 仍然是單字節(jié)編碼,它總共能表示 256 個字符。

  • GB2312

它的全稱是《信息交換用漢字編碼字符集 基本集》,它是雙字節(jié)編碼,總的編碼范圍是 A1-F7,其中從 A1-A9 是符號區(qū),總共包含 682 個符號,從 B0-F7 是漢字區(qū),包含 6763 個漢字。

  • GBK

全稱叫《漢字內(nèi)碼擴展規(guī)范》,是國家技術(shù)監(jiān)督局為 windows95 所制定的新的漢字內(nèi)碼規(guī)范,它的出現(xiàn)是為了擴展 GB2312,加入更多的漢字,它的編碼范圍是 8140~FEFE(去掉 XX7F)總共有 23940 個碼位,它能表示 21003 個漢字,它的編碼是和 GB2312 兼容的,也就是說用 GB2312 編碼的漢字可以用 GBK 來解碼,并且不會有亂碼。

  • GB18030

全稱是《信息交換用漢字編碼字符集》,是我國的強制標(biāo)準(zhǔn),它可能是單字節(jié)、雙字節(jié)或者四字節(jié)編碼,它的編碼與 GB2312 編碼兼容,這個雖然是國家標(biāo)準(zhǔn),但是實際應(yīng)用系統(tǒng)中使用的并不廣泛。

  • UTF-16

說到 UTF 必須要提到 Unicode(Universal Code 統(tǒng)一碼),ISO 試圖想創(chuàng)建一個全新的超語言字典,世界上所有的語言都可以通過這本字典來相互翻譯。可想而知這個字典是多么的復(fù)雜,關(guān)于 Unicode 的詳細(xì)規(guī)范可以參考相應(yīng)文檔。Unicode 是 Java 和 XML 的基礎(chǔ),下面詳細(xì)介紹 Unicode 在計算機中的存儲形式。

UTF-16 具體定義了 Unicode 字符在計算機中存取方法。UTF-16 用兩個字節(jié)來表示 Unicode 轉(zhuǎn)化格式,這個是定長的表示方法,不論什么字符都可以用兩個字節(jié)表示,兩個字節(jié)是 16 個 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每兩個字節(jié)表示一個字符,這個在字符串操作時就大大簡化了操作,這也是 Java 以 UTF-16 作為內(nèi)存的字符存儲格式的一個很重要的原因。

  • UTF-8

UTF-16 統(tǒng)一采用兩個字節(jié)表示一個字符,雖然在表示上非常簡單方便,但是也有其缺點,有很大一部分字符用一個字節(jié)就可以表示的現(xiàn)在要兩個字節(jié)表示,存儲空間放大了一倍,在現(xiàn)在的網(wǎng)絡(luò)帶寬還非常有限的今天,這樣會增大網(wǎng)絡(luò)傳輸?shù)牧髁?,而且也沒必要。而 UTF-8 采用了一種變長技術(shù),每個編碼區(qū)域有不同的字碼長度。不同類型的字符可以是由 1~6 個字節(jié)組成。
UTF-8 有以下編碼規(guī)則:

1、如果一個字節(jié),最高位(第 8 位)為 0,表示這是一個 ASCII 字符(00 - 7F)。可見,所有 ASCII 編碼已經(jīng)是 UTF-8 了。
2、如果一個字節(jié),以 11 開頭,連續(xù)的 1 的個數(shù)暗示這個字符的字節(jié)數(shù),例如:110xxxxx 代表它是雙字節(jié) UTF-8 字符的首字節(jié)。
3、如果一個字節(jié),以 10 開始,表示它不是首字節(jié),需要向前查找才能得到當(dāng)前字符的首字節(jié)

三、Java 中需要編碼的場景

前面描述了常見的幾種編碼格式,下面將介紹 Java 中如何處理對編碼的支持,什么場合中需要編碼。

3.1 I/O 操作中存在的編碼

我們知道涉及到編碼的地方一般都在字符到字節(jié)或者字節(jié)到字符的轉(zhuǎn)換上,而需要這種轉(zhuǎn)換的場景主要是在 I/O 的時候,這個 I/O 包括磁盤 I/O 和網(wǎng)絡(luò) I/O,關(guān)于網(wǎng)絡(luò) I/O 部分在后面將主要以 Web 應(yīng)用為例介紹。下圖是 Java 中處理 I/O 問題的接口:

這里寫圖片描述

Reader 類是 Java 的 I/O 中讀字符的父類,而 InputStream 類是讀字節(jié)的父類,InputStreamReader 類就是關(guān)聯(lián)字節(jié)到字符的橋梁,它負(fù)責(zé)在 I/O 過程中處理讀取字節(jié)到字符的轉(zhuǎn)換,而具體字節(jié)到字符的解碼實現(xiàn)它由 StreamDecoder 去實現(xiàn),在 StreamDecoder 解碼過程中必須由用戶指定 Charset 編碼格式。值得注意的是如果你沒有指定 Charset,將使用本地環(huán)境中的默認(rèn)字符集,例如在中文環(huán)境中將使用 GBK 編碼。

寫的情況也是類似,字符的父類是 Writer,字節(jié)的父類是 OutputStream,通過 OutputStreamWriter 轉(zhuǎn)換字符到字節(jié)。如下圖所示:

這里寫圖片描述

同樣 StreamEncoder 類負(fù)責(zé)將字符編碼成字節(jié),編碼格式和默認(rèn)編碼規(guī)則與解碼是一致的。

如下面一段代碼,實現(xiàn)了文件的讀寫功能:

String file = "c:/stream.txt"; 
String charset = "UTF-8"; 
// 寫字符換轉(zhuǎn)成字節(jié)流
FileOutputStream outputStream = new FileOutputStream(file); 
OutputStreamWriter writer = new OutputStreamWriter( 
outputStream, charset); 
try { 
   writer.write("這是要保存的中文字符"); 
} finally { 
   writer.close(); 
} 
// 讀取字節(jié)轉(zhuǎn)換成字符
FileInputStream inputStream = new FileInputStream(file); 
InputStreamReader reader = new InputStreamReader( 
inputStream, charset); 
StringBuffer buffer = new StringBuffer(); 
char[] buf = new char[64]; 
int count = 0; 
try { 
   while ((count = reader.read(buf)) != -1) { 
       buffer.append(buffer, 0, count); 
   } 
} finally { 
   reader.close(); 
}

在我們的應(yīng)用程序中涉及到 I/O 操作時只要注意指定統(tǒng)一的編解碼 Charset 字符集,一般不會出現(xiàn)亂碼問題,有些應(yīng)用程序如果不注意指定字符編碼,中文環(huán)境中取操作系統(tǒng)默認(rèn)編碼,如果編解碼都在中文環(huán)境中,通常也沒問題,但是還是強烈的不建議使用操作系統(tǒng)的默認(rèn)編碼,因為這樣,你的應(yīng)用程序的編碼格式就和運行環(huán)境綁定起來了,在跨環(huán)境下很可能出現(xiàn)亂碼問題。

3.2 內(nèi)存中操作中的編碼

在 Java 開發(fā)中除了 I/O 涉及到編碼外,最常用的應(yīng)該就是在內(nèi)存中進行字符到字節(jié)的數(shù)據(jù)類型的轉(zhuǎn)換,Java 中用 String 表示字符串,所以 String 類就提供轉(zhuǎn)換到字節(jié)的方法,也支持將字節(jié)轉(zhuǎn)換為字符串的構(gòu)造函數(shù)。如下代碼示例:

String s = "這是一段中文字符串"; 
byte[] b = s.getBytes("UTF-8"); 
String n = new String(b,"UTF-8");

另外一個是已經(jīng)被被廢棄的 ByteToCharConverter 和 CharToByteConverter 類,它們分別提供了 convertAll 方法可以實現(xiàn) byte[] 和 char[] 的互轉(zhuǎn)。如下代碼所示:

ByteToCharConverter charConverter = ByteToCharConverter.getConverter("UTF-8"); 
char c[] = charConverter.convertAll(byteArray); 
CharToByteConverter byteConverter = CharToByteConverter.getConverter("UTF-8"); 
byte[] b = byteConverter.convertAll(c);

這兩個類已經(jīng)被 Charset 類取代,Charset 提供 encode 與 decode 分別對應(yīng) char[] 到 byte[] 的編碼和 byte[] 到 char[] 的解碼。如下代碼所示:

Charset charset = Charset.forName("UTF-8"); 
ByteBuffer byteBuffer = charset.encode(string); 
CharBuffer charBuffer = charset.decode(byteBuffer);

編碼與解碼都在一個類中完成,通過 forName 設(shè)置編解碼字符集,這樣更容易統(tǒng)一編碼格式,比 ByteToCharConverter 和 CharToByteConverter 類更方便。

Java 中還有一個 ByteBuffer 類,它提供一種 char 和 byte 之間的軟轉(zhuǎn)換,它們之間轉(zhuǎn)換不需要編碼與解碼,只是把一個 16bit 的 char 格式,拆分成為 2 個 8bit 的 byte 表示,它們的實際值并沒有被修改,僅僅是數(shù)據(jù)的類型做了轉(zhuǎn)換。如下代碼所以:

ByteBuffer heapByteBuffer = ByteBuffer.allocate(1024); 
ByteBuffer byteBuffer = heapByteBuffer.putChar(c);

以上這些提供字符和字節(jié)之間的相互轉(zhuǎn)換只要我們設(shè)置編解碼格式統(tǒng)一一般都不會出現(xiàn)問題。

四、Java 中如何編解碼

前面介紹了幾種常見的編碼格式,這里將以實際例子介紹 Java 中如何實現(xiàn)編碼及解碼,下面我們以“I am 君山”這個字符串為例介紹 Java 中如何把它以 ISO-8859-1、GB2312、GBK、UTF-16、UTF-8 編碼格式進行編碼的。

public static void encode() { 
       String name = "I am 君山"; 
       toHex(name.toCharArray()); 
       try { 
           byte[] iso8859 = name.getBytes("ISO-8859-1"); 
           toHex(iso8859); 
           byte[] gb2312 = name.getBytes("GB2312"); 
           toHex(gb2312); 
           byte[] gbk = name.getBytes("GBK"); 
           toHex(gbk); 
           byte[] utf16 = name.getBytes("UTF-16"); 
           toHex(utf16); 
           byte[] utf8 = name.getBytes("UTF-8"); 
           toHex(utf8); 
       } catch (UnsupportedEncodingException e) { 
           e.printStackTrace(); 
       } 
}

我們把 name 字符串按照前面說的幾種編碼格式進行編碼轉(zhuǎn)化成 byte 數(shù)組,然后以 16 進制輸出,我們先看一下 Java 是如何進行編碼的。

下面是 Java 中編碼需要用到的類圖

這里寫圖片描述

首先根據(jù)指定的 charsetName 通過 Charset.forName(charsetName) 設(shè)置 Charset 類,然后根據(jù) Charset 創(chuàng)建 CharsetEncoder 對象,再調(diào)用 CharsetEncoder.encode 對字符串進行編碼,不同的編碼類型都會對應(yīng)到一個類中,實際的編碼過程是在這些類中完成的。

如字符串“I am 君山”的 char 數(shù)組為 49 20 61 6d 20 541b 5c71,下面把它按照不同的編碼格式轉(zhuǎn)化成相應(yīng)的字節(jié)。

4.1 按照 ISO-8859-1 編碼

字符串“I am 君山”用 ISO-8859-1 編碼,下面是編碼結(jié)果:

這里寫圖片描述

從上圖看出 7 個 char 字符經(jīng)過 ISO-8859-1 編碼轉(zhuǎn)變成 7 個 byte 數(shù)組,ISO-8859-1 是單字節(jié)編碼,中文“君山”被轉(zhuǎn)化成值是 3f 的 byte。3f 也就是“?”字符,所以經(jīng)常會出現(xiàn)中文變成“?”很可能就是錯誤的使用了 ISO-8859-1 這個編碼導(dǎo)致的。中文字符經(jīng)過 ISO-8859-1 編碼會丟失信息,通常我們稱之為“黑洞”,它會把不認(rèn)識的字符吸收掉。由于現(xiàn)在大部分基礎(chǔ)的 Java 框架或系統(tǒng)默認(rèn)的字符集編碼都是 ISO-8859-1,所以很容易出現(xiàn)亂碼問題,后面將會分析不同的亂碼形式是怎么出現(xiàn)的。

4.2 按照 GB2312 編碼

字符串“I am 君山”用 GB2312 編碼,下面是編碼結(jié)果:

這里寫圖片描述

GB2312 對應(yīng)的 Charset 是 sun.nio.cs.ext. EUC_CN 而對應(yīng)的 CharsetDecoder 編碼類是 sun.nio.cs.ext. DoubleByte,

GB2312 字符集有一個 char 到 byte 的碼表,不同的字符編碼就是查這個碼表找到與每個字符的對應(yīng)的字節(jié),然后拼裝成 byte 數(shù)組。查表的規(guī)則如下:

c2b[c2bIndex[char >> 8] + (char & 0xff)]

如果查到的碼位值大于 oxff 則是雙字節(jié),否則是單字節(jié)。雙字節(jié)高 8 位作為第一個字節(jié),低 8 位作為第二個字節(jié),如下代碼所示:

if (bb > 0xff) {    // DoubleByte 
           if (dl - dp < 2) 
               return CoderResult.OVERFLOW; 
           da[dp++] = (byte) (bb >> 8); 
           da[dp++] = (byte) bb; 
} else {                      // SingleByte 
           if (dl - dp < 1) 
               return CoderResult.OVERFLOW; 
           da[dp++] = (byte) bb; 
}

從上圖可以看出前 5 個字符經(jīng)過編碼后仍然是 5 個字節(jié),而漢字被編碼成雙字節(jié),在第一節(jié)中介紹到 GB2312 只支持 6763 個漢字,所以并不是所有漢字都能夠用 GB2312 編碼。

4.3 按照 GBK 編碼

字符串“I am 君山”用 GBK 編碼,下面是編碼結(jié)果:

這里寫圖片描述

你可能已經(jīng)發(fā)現(xiàn)上圖與 GB2312 編碼的結(jié)果是一樣的,沒錯 GBK 與 GB2312 編碼結(jié)果是一樣的,由此可以得出 GBK 編碼是兼容 GB2312 編碼的,它們的編碼算法也是一樣的。不同的是它們的碼表長度不一樣,GBK 包含的漢字字符更多。所以只要是經(jīng)過 GB2312 編碼的漢字都可以用 GBK 進行解碼,反過來則不然。

4.4 按照 UTF-16 編碼

字符串“I am 君山”用 UTF-16 編碼,下面是編碼結(jié)果:

這里寫圖片描述

用 UTF-16 編碼將 char 數(shù)組放大了一倍,單字節(jié)范圍內(nèi)的字符,在高位補 0 變成兩個字節(jié),中文字符也變成兩個字節(jié)。從 UTF-16 編碼規(guī)則來看,僅僅將字符的高位和地位進行拆分變成兩個字節(jié)。特點是編碼效率非常高,規(guī)則很簡單,由于不同處理器對 2 字節(jié)處理方式不同,Big-endian(高位字節(jié)在前,低位字節(jié)在后)或 Little-endian(低位字節(jié)在前,高位字節(jié)在后)編碼,所以在對一串字符串進行編碼是需要指明到底是 Big-endian 還是 Little-endian,所以前面有兩個字節(jié)用來保存 BYTE_ORDER_MARK 值,UTF-16 是用定長 16 位(2 字節(jié))來表示的 UCS-2 或 Unicode 轉(zhuǎn)換格式,通過代理對來訪問 BMP 之外的字符編碼。

4.5 按照 UTF-8 編碼

字符串“I am 君山”用 UTF-8 編碼,下面是編碼結(jié)果:

這里寫圖片描述

UTF-16 雖然編碼效率很高,但是對單字節(jié)范圍內(nèi)字符也放大了一倍,這無形也浪費了存儲空間,另外 UTF-16 采用順序編碼,不能對單個字符的編碼值進行校驗,如果中間的一個字符碼值損壞,后面的所有碼值都將受影響。而 UTF-8 這些問題都不存在,UTF-8 對單字節(jié)范圍內(nèi)字符仍然用一個字節(jié)表示,對漢字采用三個字節(jié)表示。它的編碼規(guī)則如下:

private CoderResult encodeArrayLoop(CharBuffer src, 
ByteBuffer dst){ 
           char[] sa = src.array(); 
           int sp = src.arrayOffset() + src.position(); 
           int sl = src.arrayOffset() + src.limit(); 
           byte[] da = dst.array(); 
           int dp = dst.arrayOffset() + dst.position(); 
           int dl = dst.arrayOffset() + dst.limit(); 
           int dlASCII = dp + Math.min(sl - sp, dl - dp); 
           // ASCII only loop 
           while (dp < dlASCII && sa[sp] < 'u0080') 
               da[dp++] = (byte) sa[sp++]; 
           while (sp < sl) { 
               char c = sa[sp]; 
               if (c < 0x80) { 
                   // Have at most seven bits 
                   if (dp >= dl) 
                       return overflow(src, sp, dst, dp); 
                   da[dp++] = (byte)c; 
               } else if (c < 0x800) { 
                   // 2 bytes, 11 bits 
                   if (dl - dp < 2) 
                       return overflow(src, sp, dst, dp); 
                   da[dp++] = (byte)(0xc0 | (c >> 6)); 
                   da[dp++] = (byte)(0x80 | (c & 0x3f)); 
               } else if (Character.isSurrogate(c)) { 
                   // Have a surrogate pair 
                   if (sgp == null) 
                       sgp = new Surrogate.Parser(); 
                   int uc = sgp.parse(c, sa, sp, sl); 
                   if (uc < 0) { 
                       updatePositions(src, sp, dst, dp); 
                       return sgp.error(); 
                   } 
                   if (dl - dp < 4) 
                       return overflow(src, sp, dst, dp); 
                   da[dp++] = (byte)(0xf0 | ((uc >> 18))); 
                   da[dp++] = (byte)(0x80 | ((uc >> 12) & 0x3f)); 
                   da[dp++] = (byte)(0x80 | ((uc >>  6) & 0x3f)); 
                   da[dp++] = (byte)(0x80 | (uc & 0x3f)); 
                   sp++;  // 2 chars 
               } else { 
                   // 3 bytes, 16 bits 
                   if (dl - dp < 3) 
                       return overflow(src, sp, dst, dp); 
                   da[dp++] = (byte)(0xe0 | ((c >> 12))); 
                   da[dp++] = (byte)(0x80 | ((c >>  6) & 0x3f)); 
                   da[dp++] = (byte)(0x80 | (c & 0x3f)); 
               } 
               sp++; 
           } 
           updatePositions(src, sp, dst, dp); 
           return CoderResult.UNDERFLOW; 

UTF-8 編碼與 GBK 和 GB2312 不同,不用查碼表,所以在編碼效率上 UTF-8 的效率會更好,所以在存儲中文字符時 UTF-8 編碼比較理想。

五、幾種編碼格式的比較

對中文字符后面四種編碼格式都能處理,GB2312 與 GBK 編碼規(guī)則類似,但是 GBK 范圍更大,它能處理所有漢字字符,所以 GB2312 與 GBK 比較應(yīng)該選擇 GBK。UTF-16 與 UTF-8 都是處理 Unicode 編碼,它們的編碼規(guī)則不太相同,相對來說 UTF-16 編碼效率最高,字符到字節(jié)相互轉(zhuǎn)換更簡單,進行字符串操作也更好。它適合在本地磁盤和內(nèi)存之間使用,可以進行字符和字節(jié)之間快速切換,如 Java 的內(nèi)存編碼就是采用 UTF-16 編碼。但是它不適合在網(wǎng)絡(luò)之間傳輸,因為網(wǎng)絡(luò)傳輸容易損壞字節(jié)流,一旦字節(jié)流損壞將很難恢復(fù),想比較而言 UTF-8 更適合網(wǎng)絡(luò)傳輸,對 ASCII 字符采用單字節(jié)存儲,另外單個字符損壞也不會影響后面其它字符,在編碼效率上介于 GBK 和 UTF-16 之間,所以 UTF-8 在編碼效率上和編碼安全性上做了平衡,是理想的中文編碼方式。

到此這篇關(guān)于java基礎(chǔ)之字符串編碼知識點總結(jié)的文章就介紹到這了,更多相關(guān)java字符串編碼內(nèi)容,請搜索W3Cschool以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持!

0 人點贊