字符集和字符编码

什么是字符集

ASCII(American Standard Code for Information Interchange)

1
2
man ascii
0 ~ 2^7 = 128

欧洲语系 ISO8859

  • ISO8859-1英语、法语、德语;ISO8859-5 俄语

中国的GB2312和GBK,以及中国台湾的Big5

多语系 Unicode

  • 最初的目的是把世界各地的语言都映射到16位空间
  • 8位转换为16位,称为UCS-2 (2 byte Universal Character Set), 也叫做Basic Multilingual Plane (BMP)
  • 16位到21位,有效编码区间0 ~ 0x10ffff

    The Unicode standard describes how characters are represented by code point

什么是编码方式

取“鄢”这个字的Unicode编码


1
2
echo "鄢" | native2ascii -encoding utf8
;; \u9122 这个是 code point

一段查看汉字编码的Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.nio.charset.Charset;
import java.util.Arrays;

class Main {
public static void main(String[] args) throws Exception {
unicode("鄢", "utf8");
// How many bytes: 3 What are they: [-23, -124, -94]

unicode("鄢", "utf16");
// How many bytes: 4 What are they: [-2, -1, -111, 34]

unicode("鄢", "utf-16BE");
// How many bytes: 2 What are they: [-111, 34]

unicode("🐶", "utf-16BE");
// How many bytes: 4 What are they: [-40, 61, -36, 54]

p("🐶".length());
// 2
}

public static void unicode(String s, String encoding){
p("How many bytes: " + s.getBytes(Charset.forName(encoding)).length);
p("What are they: " + Arrays.toString(s.getBytes(Charset.forName(encoding))));
}

public static void p(Object s) {
System.out.println(s);
}
}

UTF-8变长编码方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
\u9122
1110xxxx 10xxxxxx 10xxxxxx

9 -> 1001
1 -> 0001
2 -> 0010
2 -> 0010

10010001 00100010

原码
11101001 10000100 10100010

取反
10010110 11111011 11011101

补码
10010111 11111100 11011110
16+4+2+1=23 127-3=124 64+16+8+4+2=94
[-23, -124, -94]
  • 从程序运行的结果来看,UTF-8中,“鄢”占据了3个字节,且在计算机中的补码表示分别为-23, -124, -94
  • \u9122占据2个字节,但是带入UTF-8编码提供的1110xxxx 10xxxxxx 10xxxxxx模板中就变成了3个字节,我们是从低位带入模板的,x的个数刚好是16位、2个字节;
  • UTF-8编码除了和ASCII码兼容部分(以0开头,0xxxxxxx),其余都遵循一个简单的标准:
    • 其模板遵循110xxxxx 10xxxxxx,高位1的个数即是总的字节数,这里110xxxxx中11表示总共有两个字节;
    • 低位的10xxxxxx始终以10开头;
    • 这样的编码方式,使得程序清楚知道那个地方是字符开始的地方,所以才说UTF-8的单字节的编码方式。

但是UTF-8也有自己的缺点

  • 浪费内存,几乎所有的汉字都占三个字节
  • 随机访问,同字符串的长度成正比

Tips:

正数的补码就是其本身
负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1

大端字节序和小端字节序(网络字节序和主机字节序)

我们知道UTF-16的目的和Unicode本来的目的是一致的 —— “把世界各地的语言都映射到16位空间”。
\u9122为例,我们运行程序的结果应该是2个字节,但是事实上是4个字节(*How many bytes: 4 What are they: [-2, -1, -111, 34]*),这多出来的2个字节是怎么回事?
这里就是字节序在作祟。在内存存储时,如果最低有效位在最高有效位的前面,则称小端序;反之则称大端序。大端字节序又称网络字节序。
举个例子

1
2
3
\u9122 ;;;->鄢
91是最高有效位
22是最低有效位

我们观察一下返回的4个字节中的前两个字节-2, -1,它们其实不是字符“鄢”的一部分,它们是Unicode中的大端字节序的标识BOM(Bytes Order Marks)

1
2
3
4
5
6
7
0xfeff
11111110 11111111
反码
10000001 10000000
补码
10000010 10000001
-2 -1

Unicode在对待字节序时,采取了大端和小端序共存的方式。0xfeff这个编码在对调字节序后得到的0xfffe在Unicode不存在,Unicode借助这个特点来判断读入的字符到底是大端序还是小端序。
另外,从程序中可知,Java会默认将读入的字符串当做大端序处理;所以当我们明确指定UTF-16BE后,程序就会返回2个字节的结果了。

UTF-16不足以表示世界所有的字符

正如前面提及的UCS-2 (2 byte Universal Character Set)不足以表示世界上所有的字符,比如“🐶”这种emoji的字符需要4个字节才能表示,两个字节的UTF-16就无能为力了。
Java默认采用了UTF-16编码,结果导致"🐶".length()不是返回1,而是2。不得已,Java引入了codePoint这种处理方式,有点无奈。

UCS和CSI

我们计算机的程序会采用两种方式来处理不同的字符集

  • UCS:(Universal Character Set,泛用字符集)程序输入输出的时候,需要将文本数据变成UCS,统一处理。

  • CSI:(Character Set Independent 字符集独立),不对文字集做变换,直接处理。

结论

  • 积极采用UTF-8。