字符集与字符编码
字符集:指的是一堆字符及其对应编号的集合,如:Unicode、ASCII、GBXXX
字符编码:指的是如何将某个字符的对应编号转换为计算机存储的方式,如:UTF-8等等
ASCII及其扩展
ASCII 码总共有128个,使用单字节表示,但由于不够用,因此之后陆续对ASCII产生了扩展,其中ISO-8859-1涵盖了大多数西欧语言字符,所以应用的最广泛,但仍然是单字节编码,总共能表示256个字符。
GBXXX
GBXXX是中国制订的汉字字符集和编码,以 GB2312 为例,既代表字符集也代表字符编码,该字符集收录的字符较少,所以使用 1~2 个字节编码。
-
对于 ASCII 字符,使用一个字节存储,并且该字节的最高位是 0,即0-127和原来ASCII的意义相同,叫”半角”字符。
-
对于中国的字符,使用两个字节存储,并且规定每个字节的最高位都是 1,即两个大于127的字符连在一起时就表示一个汉字,叫”全角”字符。
Unicode
Unicode编码为表达任意语言的任意字符而设计,使用4字节(32位)的数字来表达全世界各个国家语言里的每个字母、符号(UNICODE编码不超过0x10FFFF,即不超过21位)。
可以理解为Unicode是字符集,而UTF-32/ UTF-16/ UTF-8是其三种字符编码方案
UTF-32
因为Unicode使用4字节来表示一个字符,所以UTF-32就是直接使用4个字节来存储Unicode字符的一种编码方案。
优点:由于每4个字节为一个字符,因此获取字符串里的第N个字符的时间复杂度是O(1)。
缺点:占用较多空间。
可能会好奇:明明unicode不超过21位,那么使用3个字节(24位)就可以了,为何还要用4个字节的UTF-32呢?关于这个问题,需要看下面的UTF-16
UTF-16
由于大部分情况下不会用到65535以后的字符。因此,就有了另外一种Unicode编码方式,叫做UTF-16(16位,2字节)。
UTF-16将0–65535范围内的字符编码成2个字节,如果真的需要表达那些很少使用的超过这65535范围的Unicode字符,则需要使用技巧编码成4个字符,所以UTF-16也是变长编码,具体方案为:
-
如果字符编码U小于0x10000(0000 0000 ~ 0000 FFFF),也就是十进制的0到65535之内,则直接使用两字节表示;
-
如果字符编码U大于0x10000(0001 0000 ~ 0010 FFFF),共有0xFFFFF个编码,需要20个bit就可以表示这些编码,将其前 10 bit作为高位和16 bit的数值0xD800进行 逻辑or 操作,将后10 bit作为低位和0xDC00做 逻辑or 操作,这样使用了4个字节来表示这个字符的编码。
Unicode 编号范围(十六进制) |
具体的 Unicode 编号(二进制) |
UTF-16 编码 |
编码后的字节数 |
---|---|---|---|
0000 0000 ~ 0000 FFFF | xxxx xxxx xxxx xxxx | xxxx xxxx xxxx xxxx | 2 |
0001 0000 ~ 0010 FFFF |
x xxxx xxxx xxxx xxxx xxxx 共有0xFFFFF个码,需要20个bit |
110110yy yyyyyyyy 110111xx xxxxxxxx (前10bit为y,后10bit为x) |
4 |
之所以可以使用110110yy yyyyyyyy 110111xx xxxxxxxx来表示超出65535范围的字符,是因为所有以110110或110111开头的 Unicode 编码(0xD800~0xDFFF)是特别为四字节的UTF-16编码预留的,这个区间内没有收录任何字符。
优点:在空间效率上比UTF-32高,因为常用字符只需要2个字节来存储
缺点:UTF-16 存在大小端字节序问题(会在之后的BOM章节进行阐述)
UTF-8
UTF-8 使用1至4个字节为每个字符编码,是一种针对Unicode的可变长度字符编码,它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部份修改,即可继续使用。
- 128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。
- 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要二个字节编码(Unicode范围由U+0080至U+07FF)。
- 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字)使用三个字节编码。
- 其他极少使用的Unicode辅助平面的字符使用四字节编码。
Unicode符号范围(十六进制) |
UTF-8编码方式(二进制) |
---|---|
00000000至0000007F | 0XXXXXXX |
00000080至000007FF | 110XXXXX 10XXXXXX |
00000800至0000FFFF | 1110XXXX 10XXXXXX 10XXXXXX |
00010000至0010FFFF | 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX |
优点:UTF-8兼容ASCII,这是UTF-32和UTF-16都没有的;相比UTF-16也不再存在字节顺序的问题;具有良好的语种支持;使用英文和西文符号比较多的场景下(例如 HTML/XML),编码较短;由于是变长,字符空间足够大,未来 Unicode 新标准收录更多字符,UTF-8 也能兼容
缺点:因为每个字符使用不同数量的字节编码,所以寻找串中第N个字符的时间复杂度是O(n)。
BOM
对于UTF-32和UTF-16编码方式还有一些其他不明显的缺点,不同的计算机系统会以不同的顺序保存字节,这意味着字符如
U+4E2D
在UTF-16编码方式下可能被保存为4E 2D或者2D 4E,这取决于该系统使用的是大尾端(big-endian)还是小尾端(little-endian)。
为了解决这个问题,多字节的Unicode编码方式定义了一个”字节顺序标记(Byte Order Mark)”,它是一个特殊的非打印字符,你可以把它包含在文档的开头来指示你所使用的字节顺序。
对于UTF-16,如果收到一个以字节FF FE开头的UTF-16编码的文档,由于FF比FE大,说明大的在前,小的在后,那么收到4E2D后就保存4E2D;反之,如果收到FE FF,说明小的在前,大的在后,那么收到4E2D就保存为2D4E。
Unicode编码 |
BOM |
---|---|
UTF-8 without BOM | 无 |
UTF-8 with BOM | EF BB BF |
UTF-16LE | FF FE |
UTF-16BE | FE FF |
UTF-32LE | FF FE 00 00 |
UTF-32BE | 00 00 FE FF |
另:由于中文windows系统的默认字符集为GBK,在写文件给windows用户下载/读取的时候,可以加上BOM,这样用户在打开文件的时候,大部分程序会读取到BOM从而使用指定的编码方式打开,这样可以避免中文乱码的情况。
byte[] headerBuffer = new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
out = response.getOutputStream();
out.write(headerBuffer);
java中的字符
java中的String代表字符串,一个字符串其实就是字符数组,
length()
方法和
charAt()
方法都是针对字符。
java中使用char代表字符,java内码使用UTF-16(历史原因,Unicode一开始是设计成2字节的),一个char永远都是两个字节,对于超出Unicode 65535之后的字符,java需要使用两个char来存储(遵从UTF-16的逻辑,使用两个0xD800~0xDFFF的数来表示)。如果声明一个char变量为需要用两个char表示的字符,那么会报错
// error: Too many characters in character literal
// char c = '𠀁';
String a = "𠀁";
System.out.println(a.length());
for (int i = 0; i < a.length(); i++) {
System.out.println((long)a.charAt(i));
}
System.out.println(Arrays.toString(a.getBytes(StandardCharsets.UTF_8)));
// 输出
// 2
// 55360
// 56321
// [-16, -96, -128, -127]
// 𠀁的Unicode码是U+20001
// 这里的55360(D840)和56321(DC01)就是遵从UTF-16的规定将20001转换为两个0xD800~0xDFFF的数
// 由于getBytes()传入的是UTF-8,此时,会先根据55360和56321求出Unicode码值为U+20001,再根据UTF-8的规则,属于00010000至0010FFFF区间,因此会最终输出4个字节
java内部一直使用Unicode码,哪怕使用
new String("你好".getBytes("gbk"), "gbk")
在jvm中依然保存的是“你好”的Unicode码。
在使用
getBytes("gbk")
时,java会将传入字符串的Unicode码转成GBK码,该逻辑是在
sun.nio.cs.ext.DoubleByte
类中实现的,具体怎么转的以后再研究。
byte只有8位,因此如果直接类型转换,对于0-255的字符不会有影响,但是对于
(byte) '中'
会丢失信息,原因是‘中’字的Unicode编码是(U+4E2D 0100 1110 0010 1101),已经超出了255,因此如果强制类型转换只会保留后8位即:0010 1101 = 45。java的
java.io.DataOutputStream
中的
writeBytes(String s)
方法就有这个问题,会导致中文出现乱码:
public final void writeBytes(String s) throws IOException {
int len = s.length();
for (int i = 0 ; i < len ; i++) {
out.write((byte)s.charAt(i));
}
incCount(len);
}
因此在使用DataOutputStream时最好不要使用该方法。
HTTP中的字符集与编码
-
Accept-Charset:浏览器申明自己接收的字符集,这就是本文前面介绍的各种字符集和字符编码,如gb2312,utf-8(通常我们说Charset包括了相应的字符编码方案);
-
Accept-Encoding:浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate),(注意:这不是只字符编码);
-
Accept-Language:浏览器申明自己接收的语言。语言跟字符集的区别:中文是语言,中文有多种字符集,比如big5,gb2312,gbk等等;
-
Content-Type:WEB服务器告诉浏览器自己响应的对象的类型和字符集。例如:Content-Type: text/html; charset=‘gb2312’
-
Content-Encoding:WEB服务器表明自己使用了什么压缩方法(gzip,deflate)压缩响应中的对象。例如:Content-Encoding:gzip
-
Content-Language:WEB服务器告诉浏览器自己响应的对象的语言。