1. 概述

Java在内存中默认使用Unicode存储字符(Java 8使用Unicode 6.2.0),

Unicode编码范围是0x00000 ~ 0x10FFFF, 其中0x0000 ~ 0xFFFF称为基础多语言编码,0x10000 ~ 0x10FFFF称为扩展多语言编码,总共能表示111万(1114112)字符,0x10FFFF转为二进制,最多21位。

Unicode里的一个字符,对应一个编码,这个编码我们成为代码点(CodePoint)。

UTF-8和UTF-16只是对Unicode的不同组织方式,Unicode的一个代码点,可以组织成UTF-8里多个字节,或组织成UTF-16里的多个字符(char)。

Java里的char就是对UTF-16的一种表现方式,Java内部的字符都是使用UTF-16展示的,对于一个特殊字符(如’𠀸’),无法用一个char表示,这个时候在Java内部只会显示为"\uD840\uDC58"。

2. 编码方式UTF-8

UTF-8的目的是为了日常存储字符串的时候减少字节数的占用,代码点(0~127)直接的字符用一个字节就能表示。 四个字节就能完整的表示21位的Unicode。

字节数格式10x0xxxxxxx20x110xxxxx 0x10xxxxxx30x1110xxxx 0x10xxxxxx 0x10xxxxxx40x11110xxx 0x10xxxxxx 0x10xxxxxx 0x10xxxxxx50x111110xx 0x10xxxxxx 0x10xxxxxx 0x10xxxxxx 0x10xxxxxx比如"汉"这个字,对应的Unicode的CodePoint的是27721,转成二进制为:0b0110110001001001,从右往左按6位截取,分别是:

0b10_001001

0b10_110001

0b1110_0110

组成数组,打印UTF-8编码对应的汉字:

System.out.println("ShowText:" + new String(new byte[]{(byte) 0b11100110, (byte) 0b10110001, (byte) 0b10001001}, "utf-8"));

# 输出

ShowText:汉

3. 编码方式UTF-16

UTF-16将0xD800 ~ 0xDFFF保留做4字节的UTF-16使用, 0x0000 ~ 0xD7FF和0xE000 ~ 0xFFFF的UTF-16可以用两个字节表示。

对于0x10000 ~ 0x10FFFF之间的Unicode用4个字节表示,现用Unicode减去0x10000,最大值0x10FFFF - 0x10000,结果为0xFFFFF,需要20位表示。

0xD800~0xDFFF用于指示当前代码点需要4个字节表示, 又被拆成了两部分, 0xD8~0xDB用于表示第一个UTF-16,前6位二进制(0b110_110) , 0xDC~0xDF表示第二个UTF-16,前6位二进制位(0b110_111)。

还是以一个例子来说,'𠀐’对应unicode:0x20010,大于0xFFFF,所以需要两个UTF-16表示,

首先Unicode减去0x10000,为

0x20010 - 0x10000 = 0x10010

转为二进制

0x0001 0000 0000 0001 0000

拆分为两个10位的二进制

0b0001 0000 00

0b00 0001 0000

拼接前缀后:

0x110110_0001 0000 00, 0x110111_00 0001 0000

格式化后:

0x1101 1000 0100 0000, 0x1101 1100 0001 0000

对应十六进制

0xD840 0xDC10

通过如下方式验证

String text = "\uD840\uDC10"

System.out.println(text);

"\uD840\uDC10".codePointAt(0) == 0x20010

4. Tomcat对Path和QueryString、RequestBody的编码处理

Path和QueryString、RequestBody采用了不同的编码,如IE下: PathInfo 是 UTF-8 编码而 QueryString 是经过 GBK 编码

Tomcat对URI的Path默认是使用UTF-8解码的,而参数默认使用iso-8859-1解码,常常到站获取参数了的中文乱码,代码逻辑见org.apache.catalina.connector.CoyoteAdapter的convertURI方法。

protected void convertURI(MessageBytes uri, Request request) throws IOException {

ByteChunk bc = uri.getByteChunk();

int length = bc.getLength();

CharChunk cc = uri.getCharChunk();

cc.allocate(length, -1);

Charset charset = connector.getURICharset(); // 这里的getURICharset()默认返回的是UTF-8

B2CConverter conv = request.getURIConverter();

if (conv == null) {

conv = new B2CConverter(charset, true);

request.setURIConverter(conv);

} else {

conv.recycle();

}

}