字符集与字符编码(Unicode、UTF-8、UTF-16、UTF-32的编码逻辑)

  • Post author:
  • Post category:其他




字符集与字符编码

字符集:指的是一堆字符及其对应编号的集合,如: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服务器告诉浏览器自己响应的对象的语言。



版权声明:本文为luwulu1056原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。