必须知道的几个简单概念:
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的字节序的存储方式,需要根据不同的硬件或者操作系统选择。
当然,解码可以通过编码的逆向过程来得到对应的字符。到此为止,对字符集和编码的原理有了基本的认识。