文章目錄
字符編碼與解碼(附:Java字符流與字節流源碼剖析)
1. 從二進制碼到字符
- 我們先聲明兩點,在計算機中:
- 任何一個字節都是由二進制碼組成的
- 任何一個字符都是由二進制碼組成的,一個字符根據解碼的方式不同,通常可以由字節組成(當然,字節同樣也是二進制)
- 不同的編碼/解碼規範規定了一個字符該由什麼樣的二進制碼錶示,例如:
- ASCII中,二進制碼’0110 1000’代表了字符’h’
- GBK中,二進制碼’11111111111111111111111111001010 11111111111111111111111111000000’代表了字符’世’
- UTF-8中,二進制碼’11111111111111111111111111100100 11111111111111111111111110111000 11111111111111111111111110010110’代表了字符’世’
- 通過使用編碼/解碼規範(ASCII、GBK、UTF-8等),我們可以實現字符與二進制碼的轉換
- 編碼:將字符轉爲二進制碼(計算機能夠識別,用於計算機的存儲、網絡傳輸)
- 解碼:將二進制碼轉爲字符(便於人類識別)
- 顯然,採用不同的編碼、解碼規範很可能會導致數據的編碼或解碼錯誤,例如:
- 將一個字符’世’,先用GBK規範編碼,再用UTF-8規範解碼(對照前面字符’世’的不同二進制碼可知)
2. 舉例:UTF-8中的編碼/解碼
- 首先,我們要知道UTF-8規範是按8bit(1byte)一次的方式轉換二進制碼的,並且它是變長多字節編碼:
- 一個英文字母用1byte表示
- 一個漢字用3byte表示(少部分漢字佔4byte)
2.1 UTF-8 編碼
- 那麼,我們用Java寫一份代碼來看一下吧
String content = "hello 世界"; byte[] bytes = content.getBytes("UTF-8"); // 使用UTF-8編碼,轉爲字節數組(即二進制碼) for (int i = 0; i < bytes.length; i++) { byte current = bytes[i]; System.out.println( i + " -> " + "十進制: " + current + ", 十六進制: " + Integer.toHexString(current) + ", 二進制: " + Integer.toBinaryString(current) ); }
- 輸出結果如下
0 -> 十進制: 104, 十六進制: 68, 二進制: 1101000 1 -> 十進制: 101, 十六進制: 65, 二進制: 1100101 2 -> 十進制: 108, 十六進制: 6c, 二進制: 1101100 3 -> 十進制: 108, 十六進制: 6c, 二進制: 1101100 4 -> 十進制: 111, 十六進制: 6f, 二進制: 1101111 5 -> 十進制: 32, 十六進制: 20, 二進制: 100000 6 -> 十進制: -28, 十六進制: ffffffe4, 二進制: 11111111111111111111111111100100 7 -> 十進制: -72, 十六進制: ffffffb8, 二進制: 11111111111111111111111110111000 8 -> 十進制: -106, 十六進制: ffffff96, 二進制: 11111111111111111111111110010110 9 -> 十進制: -25, 十六進制: ffffffe7, 二進制: 11111111111111111111111111100111 10 -> 十進制: -107, 十六進制: ffffff95, 二進制: 11111111111111111111111110010101 11 -> 十進制: -116, 十六進制: ffffff8c, 二進制: 11111111111111111111111110001100
- 解釋一下
- 前面6個byte是英文字符與一般符號,同ASCII的方式編碼,每個字符編碼爲1byte,例如
- ‘h’ -> ‘1101000’
- ‘e’ -> ‘1100101’
- 後面6個byte是中文漢字,每個字符編碼爲3byte,例如
- ‘世’ -> ‘11111111111111111111111111100100’和’11111111111111111111111110111000’和’11111111111111111111111110010110’
- 前面6個byte是英文字符與一般符號,同ASCII的方式編碼,每個字符編碼爲1byte,例如
2.2 UTF-8 解碼
- 我們再用二進制碼構造兩個漢字試試(UTF-8方式解碼)
byte shi1 = 0b11111111111111111111111111100100; byte shi2 = 0b11111111111111111111111110111000; byte shi3 = 0b11111111111111111111111110010110; byte[] shi = {shi1, shi2, shi3}; System.out.println(new String(shi, "UTF-8")); byte jie1 = 0b11111111111111111111111111100111; byte jie2 = 0b11111111111111111111111110010101; byte jie3 = 0b11111111111111111111111110001100; byte[] jie = {jie1, jie2, jie3}; System.out.println(new String(jie, "UTF-8"));
- 輸出如下
世 界
2.3 錯誤的編碼與解碼
- 如果我們先用GBK規範編碼,再用UTF-8規範解碼,會發生什麼事情?
String content = "hello 世界"; byte[] gbkBytes = content.getBytes("GBK"); String utf8Str = new String(gbkBytes, "UTF-8"); System.out.println(utf8Str); System.out.println(content);
- 輸出如下
hello ���� hello 世界
- 顯然,因爲GBK和UTF-8對於英文字符的解析方式相同,所以’hello '部分沒有出錯,但是因爲對於漢字的編碼/解碼規定不同,導致了亂碼
- 另外,如果一個文本文件是GBK編碼的,使用ISO-8859-1解碼,再使用ISO-8859-1編碼,最後存儲
- 使用ISO-8859-1解碼,如果打印字符,顯然是亂碼的
- 存儲的結果則是沒有問題的,因爲ISO-8859-1是單字節編碼,並且使用了單字節內的所有空間。因此將任何字節流按ISO-8859-1解碼,再編碼,都不會有丟失的問題。(MySQL默認的編碼Latin1就是如此)
- GBK與ISO-8859-1的示例代碼如下
import java.io.FileInputStream; import java.io.FileOutputStream; public class Demo02 { public static void main(String[] args) throws Exception { String content = "hello 世界"; String path = "./test.txt"; // 對字符串進行GBK編碼,並存儲 byte[] gbkBytes = content.getBytes("GBK"); saveByteArray(path, gbkBytes, 0, gbkBytes.length); // 讀取該文件字節碼 byte[] bytes = new byte[1024]; int len = readByteArray(path, bytes); System.out.println("len = " + len); // 使用GBK解碼,並打印 String gbkStr = new String(bytes, 0, len, "GBK"); System.out.println("gbkStr = " + gbkStr); // 使用iso-8859-1解碼,並打印 String isoStr = new String(bytes, 0, len, "iso-8859-1"); System.out.println("isoStr = " + isoStr); // 使用iso-8859-1對該字符串進行編碼,然後存儲 byte[] isoBytes = isoStr.getBytes("iso-8859-1"); saveByteArray(path, isoBytes, 0, isoBytes.length); } public static void saveByteArray(String path, byte[] bytes, int off, int len) throws Exception { FileOutputStream fos = new FileOutputStream(path); fos.write(bytes); fos.close(); } public static int readByteArray(String path, byte[] bytes) throws Exception { FileInputStream fis = new FileInputStream(path); int len = fis.read(bytes); fis.close(); return len; } }
3. 字符編碼的記錄
-
ASCII
- 美國信息交換標準代碼
- 7 bit 表示一個字符
- 共128個字符
-
ISO-8859-1
- 對於ASCII的擴展
- 8 bit 表示一個字符,會使用整個byte
- 共256個字符
-
GB2312
- 國標,漢字的編碼集
- 2 byte 表示一個字符
- 共6763個漢字
-
GBK
- 對於GB2312的擴展,能表示更多的字符
- 2 byte 表示一個字符
- 共21003個漢字
-
GB18030
- 對於GBK的擴展,最完整的漢字編碼集
- 變長多字節編碼,1個、2個或4個byte表示一個字符
- 共70000餘個漢字
-
BIG5
- 由臺灣制定,主要用於繁體漢字編碼
- 2 byte 表示一個字符
- 共13060個漢字
-
Unicode
- 由國際標準化組織制定,整合全世界的字符
- 2 byte 表示一個字符
- 表示全世界所有的字符
- 如果只使用英文字符,較浪費空間
-
UTF(Unicode Translation Format)
- 通用轉換格式,是Unicode的實現,解決了Unicode空間浪費的問題
- UTF-8, UTF-16, UTF-16LE(little endian), UTF-16BE(big endian), UTF-32
-
UTF-8
- 變長多字節編碼,1~4字節表示一個字符
- 1 byte 表示一個US-ASCIl字符
- 2 byte 表示一個拉丁文字符(拉丁文、希臘文、西裏爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文等)
- 3 byte 表示一個漢字(中日韓文字、東南亞文字、中東文字等)
- 4 byte 表示其他極少使用的語言
- 變長多字節編碼,1~4字節表示一個字符
-
UTF-8-BOM(Byte Order Mark)
- Unicode規定使用BOM來標識字節順序,UTF-8-BOM的文件會以EF BB BF開頭
- UTF-16和UTF-32需要決定是按2Byte讀還是按4byte讀,需要BOM來決定順序
- UTF-8是按1byte讀的,沒有字節序問題,是不需要BOM來標識字節序的
- 建議:使用UTF-8時,最好使用不帶BOM的UTF-8
4. Java中字符流與字節流的關係(源碼剖析)
- 我們從字符流入手,先看java.io.FileReader,它繼承於java.io.InputStreamReader,其構造器如下
public FileReader(String fileName) throws FileNotFoundException { super(new FileInputStream(fileName)); } public FileReader(File file) throws FileNotFoundException { super(new FileInputStream(file)); } public FileReader(FileDescriptor fd) { super(new FileInputStream(fd)); }
- 其構造器都是會new FileInputStream(),而FileInputStream繼承於InputStream
- FileInputStream會通過native方法open0獲取到輸入字節流,其代碼如下
public FileInputStream(File file) throws FileNotFoundException { String name = (file != null ? file.getPath() : null); SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkRead(name); } if (name == null) { throw new NullPointerException(); } if (file.isInvalid()) { throw new FileNotFoundException("Invalid file path"); } fd = new FileDescriptor(); fd.attach(this); path = name; open(name); } private void open(String name) throws FileNotFoundException { open0(name); } private native void open0(String name) throws FileNotFoundException;
- 接着FileReader的構造器,調用了其父類InputStreamReader的構造器(super方法),將FileInputStream傳了進去
- InputStreamReader實際是InputStream的包裝類,對其進行功能增強(提供字符解碼能力)。在構造器中,利用StreamDecoder對InputStream進行解碼
- InputStreamReader構造器代碼如下
public InputStreamReader(InputStream in) { super(in); try { sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object } catch (UnsupportedEncodingException e) { // The default encoding should always be available throw new Error(e); } }
- StreamDecoder中的解碼方法會先看是否指定了Charset(例如UTF-8),如果沒指定會使用默認的Charset,最後如果系統支持該Charset,那麼會返回StreamDecoder,代碼如下
public static StreamDecoder forInputStreamReader(InputStream var0, Object var1, String var2) throws UnsupportedEncodingException { String var3 = var2; if (var2 == null) { var3 = Charset.defaultCharset().name(); } try { if (Charset.isSupported(var3)) { return new StreamDecoder(var0, var1, Charset.forName(var3)); } } catch (IllegalCharsetNameException var5) { } throw new UnsupportedEncodingException(var3); }
- 而在InputStreamReader中,會將StreamDecoder賦值給全局變量sd,後續的讀取相關方法,皆是調用sd的read方法,代碼如下
public String getEncoding() { return sd.getEncoding(); } public int read() throws IOException { return sd.read(); } public int read(char cbuf[], int offset, int length) throws IOException { return sd.read(cbuf, offset, length); } public boolean ready() throws IOException { return sd.ready(); } public void close() throws IOException { sd.close(); }
- 顯然,我們可以知道Java中字符流與字節流的關係,其實就是字符流是對字節流功能的增強(編碼/解碼),本質上字符流用到的還是字節流