背景
前兩天在網上看到一篇關於編碼的討論,仔細學習了一下unicode,utf8,utf16的定義。這篇博客旨在讓讀者真正理解他們是什么。
什么是編碼
在閱讀本文之前建議讀者先去閱讀這篇文章:http://www.freebuf.com/articles/others-articles/25623.html,如果你沒有耐心讀完他也沒關系,只需要明白三個道理:
1,這個世界上從來沒有純文本這回事,如果你想讀出一個字符串,你必須知道它的編碼。如果你不知道一段數據流的編碼方式,你就永遠不會知道這里面的內容。
2,Unicode是一個簡單的標准,用來把字符映射到數字上。Unicode協會的人會幫你處理所有幕后的問題,包括為新字符指定編碼。我們用的所有字符都在unicode里面有對應的映射,每個映射稱為一個碼點(http://en.wikipedia.org/wiki/Code_point)
3,Unicode並不告訴你字符是怎么編碼成字節的。這是被編碼方案決定的,通過UTF來指定。
讀完前面這篇文章之后你也許就了解了一個二進制流到屏幕字符的過程:
二進制流->根據編碼方式解碼出碼點->根據unicode碼點解釋出字符->系統渲染繪出這個字符
文本字符保存到計算機上的過程:
輸入字符->根據字符找到對應碼點->根據編碼方式把碼點編碼成二進制流->保存二進制流到硬盤上
從這個過程我們可以知道能不能從二進制流讀取出字符關鍵就在於能不能找到二進制流的編碼,掌握了編碼方式的信息就可以用對應的逆過程解碼。
看到這里有讀者一定會問:為什么要編碼,根據二進制流計算碼點不好嗎?
原因是良好設計的編碼可以為我們提供很多附加的功能,包括容錯糾錯(在網絡通信中尤其重要),自同步(不必從文本頭部開始就可以解碼)等等。編碼從信息論的角度上來說就是增加了冗余的信息,冗余的這部分信息就可以為我們提供額外的功能。
utf8的編碼規則
我們來看utf8和utf16具體是如何編碼的:
Utf8有如下特點:
1.可變長編碼,由第一個字節決定該字符編碼長度
2.向下兼容ascii碼(這也是為什么用utf8編碼可以完美打開ascii文本文件)
Utf8的編碼規則:
- 一個字節的編碼完全用於ascii碼(從0-127)
- 大於127的碼點都用多字節來編碼,多字節包含開頭字節和后續字節
開頭字節以若干個1開頭(長度為幾就有幾個1,因此只要讀完開頭字節就可以知道本字符共有多少個字節),后接1個0.后續字節都以10開頭
- 從右到做,后續字節每個字節占用原碼點6個位,剩余的放在開頭字節。
- 開頭字節和后續字節不共享任何數據,因此utf8是自同步的。舉例來說我們看到一個字節以110…開頭時,我們就知道這是一個2字節的字符的開頭字節。
具體來舉幾個例子:
字符 | 碼點 | 二進制 UTF-8 | 16進制 UTF-8 | |
---|---|---|---|---|
$ | U+0024 |
0100100 |
00100100 |
24 |
¢ | U+00A2 |
000 10100010 |
11000010 10100010 |
C2 A2 |
€ | U+20AC |
00100000 10101100 |
11100010 10000010 10101100 |
E2 82 AC |
𤭢 | U+24B62 |
00010 01001011 01100010 |
11110000 10100100 10101101 10100010 |
F0 A4 AD A2 |
實現了UTF8編碼的java代碼:
public class Utf8 { /** * @param codePoint in unicode * @return corresponding utf8 bytes * @throws Exception */ private static final long RightSix = (1 << 6) - 1; private static final long PrefixForContinuasByte = 1 << 7; public static long EncodeToUtf8(long codePoint) throws Exception { if (codePoint < 0 || codePoint > 0x1FFFFF) throw new Exception("Illegal code point!"); if (codePoint <= 0x007F) { return codePoint;// ascii character } else if (codePoint <= 0x07FF) { long byte1 = (6 << 5) + (codePoint >> 6); long byte2 = PrefixForContinuasByte + (codePoint & RightSix); return (byte1 << 8) + byte2; } else if (codePoint <= 0xFFFF) { long byte1 = (14 << 4) + (codePoint >> 12); long byte2 = PrefixForContinuasByte + ((codePoint >> 6) & RightSix); long byte3 = PrefixForContinuasByte + (codePoint & RightSix); return (byte1 << 16) + (byte2 << 8) + byte3; } else { long byte1 = (30 << 3) + (codePoint >> 18); long byte2 = PrefixForContinuasByte + ((codePoint >> 12) & RightSix); long byte3 = PrefixForContinuasByte + ((codePoint >> 6) & RightSix); long byte4 = PrefixForContinuasByte + (codePoint & RightSix); return (byte1 << 24) + (byte2 << 16) + (byte3 << 8) + byte4; } } public static void main(String[] args) { try { while (true) { System.out.print("Input a number in Hex format:"); Scanner sc = new Scanner(System.in); String s = sc.nextLine(); // System.out.println("it is "+HexStringToLong(s)+" in decimal format"); long utf8 = EncodeToUtf8(HexStringToLong(s)); String hexString = Long.toHexString(utf8); System.out.println("You input " + s + " in Hex format and we encode it to utf8 character " + hexString); } } catch (Exception e) { System.out.println(e.getLocalizedMessage()); // TODO: handle exception } } }
運行結果:
Input a number in Hex format:24 You input 24 in Hex format and we encode it to utf8 character 24 Input a number in Hex format:A2 You input A2 in Hex format and we encode it to utf8 character c2a2 Input a number in Hex format:20AC You input 20AC in Hex format and we encode it to utf8 character e282ac Input a number in Hex format:24B62 You input 24B62 in Hex format and we encode it to utf8 character f0a4ada2
關於UTF-16的編碼規則,讀者可以參考這篇文章:http://en.wikipedia.org/wiki/UTF-16
這里附上UTF16-BE的編碼代碼:
public class Utf16 { /** * @param codePoint in unicode * @return corresponding utf16 bytes * @throws Numberformat Exception */ private static final long Substracted=0x10000; private static final long AddToHigh=0xD800; private static final long AddToLow=0xDC00; private static long HexStringToLong(String s) { if (s.length() == 0) return 0; long ans = 0; for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c >= '0' && c <= '9') ans = (ans << 4) + (c - '0'); else if (c >= 'A' && c <= 'F') ans = (ans << 4) + (c - 'A' + 10); else throw new NumberFormatException(); } return ans; } public static long EncodeToUtf16BE(long codePoint) throws Exception { if(codePoint<0||(codePoint<=0xDFFF&&codePoint>=0xD800)||codePoint>0x10FFFF) throw new NumberFormatException(); if(codePoint<=0xD7FF)//Basic Multilingual Plane { return codePoint; } else { long sub=codePoint-Substracted; long high=sub>>10; long low=sub&0x3FF; long word1=AddToHigh+high; long word2=AddToLow+low; return (word1<<16)+word2; } } public static void main(String[] args) { while(true) { System.out.print("Input a number in hex format"); Scanner sc=new Scanner(System.in); String s=sc.nextLine(); try { String utf16=Long.toHexString(EncodeToUtf16BE(HexStringToLong(s))); System.out.println("You input "+s+" we encode it to utf16-BE "+utf16); } catch (Exception e) { e.printStackTrace(); } } } }