必须知道的几个简单概念:
1. unicode:字符集,包含了全世界几乎所有的字符。解除了ascii、iso-8859-1等字符集的局限。
2. unicode码:与每一个字符相对应的数字,一对一映射,常用的BMP区段范围在0x0000—0xffff。编码时,通过字符找到数字,解码时,相反。
3. utf-16:编码方式,将unicode中的每个字符转换成两个字节,并与BMP保持一致。由于编码方式简单,java内部采用utf-16的编码字符。
4. utf-8:编码方式,将字符转化为1-4个字节。如英文字母会用一个字节来表示,汉字则会使用三个字节来表示。
5. gbk:字符集、编码方式。gbk拥有自己的字符集以及对应的编码方式。向下兼容gb2312,而gb18030兼容了gbk。
6. ... ...
现在来看看我们常用的一个编码方法:String.getBytes(String charset);它具体做了什么。
1. charset指定了字符集以及使用的编码方式,比如传入:gbk。
//关键代码 Charset cs = lookupCharset(csn); // 这里会找到sun.nio.cs.ext.GBK if (cs != null) se = new StringEncoder(cs, csn); // 最终通过sun.nio.cs.ext.GBK.newEncoder()方法返回gbk的encoder return se.encode(ca, off, len);
2. 通过调用encoder的ecode方法。
//关键代码 CoderResult cr = encodeLoop(in, out);// in为刚传入的字符序列,out为即将返回的字节序列
3. gbk ecoder的encodeLoop方法在其父类DoubleByteEncoder中实现。
protected CoderResult encodeLoop(CharBuffer src, ByteBuffer dst) { if (true && src.hasArray() && dst.hasArray()) return encodeArrayLoop(src, dst); else return encodeBufferLoop(src, dst); }
4. 按照array和buffer两种形式去编码,但是本质都是一样的,看其中的一个:encodeBufferLoop。
while (src.hasRemaining()) { char c = src.get(); int b = encodeSingle(c); if (b != -1) { // Single-byte character if (dst.remaining() < 1) return CoderResult.OVERFLOW; mark++; out.put((byte)b);//对单字节的处理,直接返回b对应的字节 continue; } // Double Byte character int ncode = encodeDouble(c); if (ncode != 0 && c != '\u0000') { if (dst.remaining() < 2) return CoderResult.OVERFLOW; mark++; //对双字节字符的处理 //第一个字节通过ncode的高8位组成 out.put((byte) ((ncode & 0xff00) >> 8)); //第二个字节由低8位组成 out.put((byte) ncode); continue; } }
5. 那么以上的int类型的b和ncode是怎么来的呢?
protected int encodeSingle(char inputChar) { if (inputChar < 0x80) // inputChar对应的int值小于128,即:1000 0000 return (byte)inputChar; else return -1; } protected int encodeDouble(char ch) { int offset = index1[((ch & 0xff00) >> 8 )] << 8; //关注index1 //关注index2 return index2[offset >> 12].charAt((offset & 0xfff) + (ch & 0xff)); }
6.如何判断单字节还是双字节?
通过unicode码表,找到字符inputChar以及其对应的unicode码,例如:'一'对应的值为0x4E00。如果这个值小于0x80,则为单字节字符,否则为双字节。
如果是单字节字符,直接返回该unicode码的低8位。
如果为双字节字符,则通过index1和index2来定位对应的值。
7. index1和index2分别是什么?
在gbk encoder中分别定义了index1和index2,用来找到unicode码对应的gbk编码。
index2 由7个字符串组成,每个字符串定义了不同的gbk编码,形式如下:
private final static String innerIndex0= "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"+...+ "\uA8A8\uA8A6\uA8BA\u0000\uA8AC\uA8AA\u0000\u0000"+...+ "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000"; private final static String innerIndex1= ... .... private final static String innerIndex6= private final static String index2[] = { innerIndex0, innerIndex1, innerIndex2, innerIndex3, innerIndex4, innerIndex5, innerIndex6 };
index1主要用于定位index2中的字符,代码如下:
private final static short index1[] = { 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 11, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... ... 0, 0, 0, 0, 0, 0, 0, 0, 0, 108, 109, 0, 0, 0, 110, 111 };
这样GBK编码的整个过程就完成了(其中只展示了关键步骤)。
再来看看utf-8的编码过程有什么不一样呢,直接看sun.nio.cs.UTF_8.encodeBufferLoop方法。
private CoderResult encodeBufferLoop(CharBuffer src, ByteBuffer dst) { while (src.hasRemaining()) { int c = src.get(); // unicode码 if (c < 0x80) { // 128个US-ASCII字符只需一个字节编码 dst.put((byte)c); } else if (c < 0x800) { // 2 bytes, 11 bits // 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要二个字节编码 // 以11开头,代表由双字节组成 dst.put((byte)(0xc0 | ((c >> 06)))); dst.put((byte)(0x80 | (c & 0x3f))); } else if (Surrogate.is(c)) { // 极少使用的Unicode 辅助平面的字符使用四字节编码 // Have a surrogate pair if (sgp == null) sgp = new Surrogate.Parser(); int uc = sgp.parse((char)c, src); // 以1111开头,代表由4个字节组成 dst.put((byte)(0xf0 | ((uc >> 18)))); dst.put((byte)(0x80 | ((uc >> 12) & 0x3f))); dst.put((byte)(0x80 | ((uc >> 06) & 0x3f))); dst.put((byte)(0x80 | (uc & 0x3f))); mark++; //2 chars } else { // 3 bytes, 16 bits // 其他BMP中的字符(这包含了大部分常用字)使用三个字节编码,中文字符一般使用3个字节编码。 // 以111开头,代表由三个字节字节组成 dst.put((byte)(0xe0 | ((c >> 12)))); dst.put((byte)(0x80 | ((c >> 06) & 0x3f))); dst.put((byte)(0x80 | (c & 0x3f))); } } return CoderResult.UNDERFLOW; }
可以看到,utf-8并没有像gbk中index1和index2的定义。utf-16也是一样,也就是说utf编码并不会去查询其他的码表。类似gbk编码方式的还有很多,例如:韩文编码等。从这里可以看出,utf-8编码的方式更加简单,少了查询码表的过程,所以性能会更高一些。但是,对于中文等字符,utf-8采用3个字节来存储,占用了更多的存储空间,用于网络传输会占用更大的带宽,所以各有利弊。
utf-16则采用了unicode默认的编码方式,即直接将unicode码放入两个字节中,并分为big-endian和little-endian的字节序的存储方式,需要根据不同的硬件或者操作系统选择。
当然,解码可以通过编码的逆向过程来得到对应的字符。到此为止,对字符集和编码的原理有了基本的认识。