只是不愿随波逐流 ...|

lidongdongdong~

园龄:2年7个月粉丝:14关注:8

7、字符

内容来自王争 Java 编程之美

字符、字符串是编程语言中必不可少的语法,Java 和 C / C++ 都用 char 来存储字符,用 char 数组(char[])来存储字符串
当然,在 Java 中,存储字符串还可以使用 String 类,这个下节再讲
在处理字符、字符串时,绝大多数程序员都遇到过乱码问题,因为对底层原理掌握不牢,很多程序员面对乱码手足无措,乱改瞎试
所以,本节我们就详细讲讲字符和字符编码
在开始之前,我们还是留一个思考题给你:为什么 C / C++ 中 char 类型占 1 个字节长度,而 Java 中的 char 类型占 2 个字节长度

1、字符、字符集和字符编码

字符、字符集和字符编码是我们常常一块听到的几个词语,很多人对这几个词语的区别,特别是字符集和字符编码的区别,不是很清楚,接下来,我们就先介绍一下它们

字符(Character)可以理解为书面表达中所可能用到的符号,包括各种文字、数字、标点、图形符号、控制符号(如回车换行等,待会会讲)等

字符集(Character Set)是一组字符的集合,不同语言会有不同的字符集
比如,GB2312 就是中文字符集,包含 6000 多个简化汉字和一些符号、序号、数字、字母、拼音等共7000多个字符,涵盖了中文书面表达所需的大部分字符
字符集不仅包含字符,还包含每个字符的编号,这里的编号只是方便索引,跟字符编码不是一回事

字符编码(Character Encoding)是指计算机存储字符编号的格式
大部分情况下,在设计字符集时,会同步设计字符编码,一个字符集会对应一种字符编码,比如,GB2312 字符集对应 GB2312 字符编码
不过,也有例外,同一个字符集也可以对应多种不同的字符编码,比如,Unicode 字符集对应 UTF-8、UTF-16、UTF-32 三种不同的字符编码

2、常见字符集和字符编码

比较常用的字符集有 ASCII、GB2312、GBK、GB18030、Unicode,其中,前三个的字符集跟字符编码同名,也即是字符集和字符编码是一一对应的
Unicode 字符集对应的字符编码有三种,分别是 UTF-8、UTF-16、UTF-32,接下来,我们就详细介绍一下这几种常用的字符集和字符编码

2.1、ASCII 字符集和字符编码

ASCII 全称为 American Standard Code for Information Interchange,中文翻译为:美国信息交换标准代码
从名称上也可以看出,这套字符集和字符编码是美国人设计的,主要包含了英文系统的计算机所用到的字符

ASCII 字符集只包含 128 个字符,对应的编号如下表所示,因为只有 128 个字符,所以,ASCII 字符编码很简单,使用 1 个字节中的低 7 位来存储编号,最高位默认为 0

ASCII 码表
33 ~ 126 可打印字符
A ~ Z:65 ~ 90,a ~ z:97 ~ 122
大写转小写 +32,小写转大写 -32

ASCII 值 控制字符 ASCII 值 控制字符 ASCII 值 控制字符 ASCII 值 控制字符
0 NUT 32 (space) 64 @ 96
1 SOH 33 ! 65 A 97 a
2 STX 34 " 66 B 98 b
3 ETX 35 # 67 C 99 c
4 EOT 36 $ 68 D 100 d
5 ENQ 37 % 69 E 101 e
6 ACK 38 & 70 F 102 f
7 BEL 39 , 71 G 103 g
8 BS 40 ( 72 H 104 h
9 HT 41 ) 73 I 105 i
10 LF 42 * 74 J 106 j
11 VT 43 + 75 K 107 k
12 FF 44 , 76 L 108 l
13 CR 45 - 77 M 109 m
14 SO 46 . 78 N 110 n
15 SI 47 / 79 O 111 o
16 DLE 48 0 80 P 112 p
17 DCI 49 1 81 Q 113 q
18 DC2 50 2 82 R 114 r
19 DC3 51 3 83 S 115 s
20 DC4 52 4 84 T 116 t
21 NAK 53 5 85 U 117 u
22 SYN 54 6 86 V 118 v
23 TB 55 7 87 W 119 w
24 CAN 56 8 88 X 120 x
25 EM 57 9 89 Y 121 y
26 SUB 58 : 90 Z 122 z
27 ESC 59 ; 91 [ 123 {
28 FS 60 < 92 / 124 |
29 GS 61 = 93 ] 125 }
30 RS 62 > 94 ^ 126 `
31 US 63 ? 95 _ 127 DEL

从上表中,我们可以看出,ASCII 字符集中的字符分为两类:不可显示字符和可显示字符

  • 编号 0 ~ 31 和 127 对应的字符为不可显示字符,编号 32 ~ 126 对应的字符为可显示字符
  • 不可显示字符也叫做控制字符,当在一个字符串中包含一些控制字符时,控制字符并不会显示在计算机屏幕上,而是控制输出格式
    比如常用的控制字符有回车符(ASCII 码值为 13)、换行符(ASCII 码值为 10)

在字符串中存储可显示字符比较简单,但如何存储非可显示字符呢?

我们可以使用 \xxx 的格式来表示非可显示字符,其中,xxx 为非可显示字符的 ASCII 码的八进制表示
当然,对于可显示字符,我们也可以用这种方式来表示,示例代码如下所示

char visibleC1 = 'a';
char visibleC2 = '\141'; // a 的 ASCII 为 97, 八进制 141
System.out.println(visibleC2); // 打印 a
char invisibleC3 = '\012'; // 换行的 ASCII 为 10, 八进制 012
System.out.println("abc\012def"); // 一行打印 abc, 另一行打印 def

实际上,对于部分常用的非可显示字符,我们还可以使用转义字符来表示,比如 \r 表示回车,\n 表示换行,\t 表示tab,\0 表示 null

char invisibleC4 = '\n'; // 换行的转移字符为 \n
System.out.println("abc\ndef"); // 一行打印 abc, 另一行打印 def

当然,我们也可以直接将 ASCII 码值来表示字符,如下代码所示
之所以可以这样来做,是因为字符 a 的 ASCII 码值为 97,它存储在计算机中的二进制串,跟数值 97 存储在计算机中的二进制串,是一模一样的,都是 0110 0001
对于 0110 0001 这个二进制串,到底是表示为字符 a,还是数值 97,全看编译器如何解读

char ch = 97;
System.out.println(ch); // 打印: a

实际上,char 类型数据之间还可以进行比较操作,对应的就是,将字符编码转变为无符号数之后进行大小比较
除此之外,char 类型数据还可以进行加减操作,对应的就是,将字符编码转变为无符号数之后的加减操作,示例代码如下所示,将字符串 "231" 转化为整数 231

public int convert(char[] chs, int n) {
int res = 0;
for (int i = 0; i < n; i++) {
res = res * 10 + (chs[i] - '0');
}
return res;
}

2.2、GB* 系列字符集和字符编码

ASCII 只能表示 128 个字符,对于英文来说可能足够了,但是,对于中文、日文、韩文等,所包含的字符远远不止这些
所以,当计算机传到世界各地之后,为了适应各地的语言,又相继发布了其他字符集和字符编码,支持中文的字符集和字符编码,大都以 GB 开头来命名
比如常见的有 GB2312、GBK、GB18030

GB2312 发布于 1980 年,是第一个中文字符集和字符编码的,它采用定长存储方式,每个字符编号都用 2 个字节来存储
尽管 2 个字节可以表示 6 万多(2 ^ 16)个不同的字符,但因为其特殊的编码方式,GB2312 仅收录了 6000 多个汉字及其他符号,总共 7000 多个字符

尽管 GB2312 收录了使用频率超过 99% 的常用汉字,但对于一些罕用字、人名等,GB2312 无法表示,毕竟中国汉字有 10 万多个,显然,GB2312 是不够全面的
于是就出现了 GBK,尽管 GBK 仍然使用 2 个字节,但因为其使用新的编码方式(对于 GB* 字符集的编码方式,我们不展开讲解),能表示的字符增多,比 GB2132 增加了 2 万多个汉字和符号

GB18030 兼容 GB2132 和 GBK,并且可表示的字符更多,共收录了 7 万多个汉字
GB18030 采用变长编码方式,不同的字符使用不同长度的字节(1 字节、2 字节或 4 字节)来存储,存储的字节长度是不同的
关于变长编码和定长编码的编码原理和优缺点,我们在 Unicode 字符集及其 3 种字符编码中讲解

2.3、Unicode 字符集和 UTF* 系列字符编码

各个语言都有自己的字符集和字符编码,同一串二进制位在不同的字符集和字符编码中,代表不同的字符
这就导致我们无法在一个文档中使用两种不同的语言(不同的字符集和字符编码)

为了大一统,Unicode 字符集就出现了,Unicode 字符集包含大约 100 万个字符,涵盖了世界上所有语言的所有字符,每一个字符都对应一个不同的编号
使用 Unicode 字符集,我们就能在同一个文档里使用不同语言的字符了,我们一般习惯将字符编号表示为十六进制,并且辅以前缀 "U+",以表示此编号为 Unicode 字符编号

尽管 Unicode 字符集中的字符个数超百万,但常用的并不多,为了让常用字符的编号尽可能小(这样计算机在存储时会节省空间,待会会讲),Unicode 字符集将编号分为两部分

  • 编号从 U+0 ~ U+FFFF,并且排除 U+D800 ~ U+DFFF,分配给使用频率最高的字符,这几乎涵盖了各个语言中的常用字符
    至于为什么要排除 U+D800 ~ U+DFFF这个范围的编号,我们在讲完 UTF-16 字符编码后你就明白了
  • 编号从 U+10000 ~ U+10FFFF,大约有 100 多万个编号,分配给剩下的所有字符

Unicode 只是一个字符集,包含字符及其编号,但并不包含字符编号在计算机中的存储方式,也就是字符编码
按照编码的复杂程度,我们来依次讲解 Unicode 字符集对应的 3 种字符编码:UTF-32、UTF-16、UTF-8

2.3.1、UTF-32

UTF-32 是定长编码,使用 4 个字节来存储 Unicode 编号
定长的好处就是编码简单,只需要将字符编号直接存入计算机即可
读取时解码也非常简单,每读取四个字节解码为一个字符

2.3.2、UTF-16

UTF-16 采用变长编码,U+0 ~ U+FFFF 范围(不包含 U+D800 ~ U+DFFF)内的编号使用 2 字节编码,U+10000 ~ U+10FFFF 之间的编号采用 4 字节编码
采用变长编码方式,比起定长的 UTF-32 编码方式,更加节省存储空间,但是,编解码也复杂了很多
当从一个文本中读取 2 个字节之后,我们怎么知道这 2 个字节对应的数值,是 U+0 ~ U+FFFF 范围的 2 字节编码,还是 U+10000 ~ U+10FFFF 范围内的 4 字节编码的高十六位或低十六位呢?

为了解决这个问题,UTF-16 将 U+0 ~ U+FFFF 之间的 Unicode 编号,直接存储在 2 个字节中,而对于 U+10000 ~ U+10FFFF 之间的 Unicode 编号,采用如下特殊编码方式

  • STEP 1:将 U+DC00 ~ U+DFFF 范围内的 Unicode 编号减去 10000,得到新的范围:U+00000 ~ U+FFFFF
    新的范围内的每个编号,只使用 20 个二进制位就能表示
  • STEP 2:将 20 个二进制位中的高 10 位取出,放到 UTF-16 的 4 字节编码中的高 16 位中
    前面多出的 6 位用 110110 补全,这样高 16 位的数据范围就变成了 U+D800 ~ U+DBFF
  • STEP 3:将 20 个二进制位中的低 10 位取出,放到 UTF-16 的 4 字节编码中的低 16 位中
    前面多出的 6 位用 110111 补全,这样低 16 位的数据范围就变成了 U+DC00 ~ U+DFFF

image
因为 UTF-16 最小的编码长度是两字节,所以,在将二进制编码解码为字符时,我们会每次从文本中读取两个字节来分析

  • 如果这两个字节的数值落在 U+D800 ~ U+DBFF 范围之间(也就是前缀为 1101 10),那么读出的这两个字节就是 4 字节编码的高 16 位
  • 如果这两个字节的数值落在 U+DC00 ~ U+DFFF 范围之间(也就是前缀为 1101 11),那么读出的这两个字节就是 4 字节编码的低 16 位
  • 如果这两个字节的数值不落在 U+D800 ~ U+DFFF 范围内(U+D800 ~ U+DBFF 和 U+DC00 ~ U+DFFF),那么读出的这两个字节就是 2 字节编码

还记得前面提到,在 Unicode 字符集中,在 U+0 ~ U+FFFF 这个范围内
U+D800 ~ U+DFFF 这个范围的编号并没有使用,没有对应的字符,原因就是 2 字节编码跟 4 字节编码的高 16 位和低 16 位数据做区分

实际上,在 UTF-16 编码中,4 字节编码的低 16 位并不需要特殊标识,因为对于一个正确编码了的文件,每次读取 2 字节之后
如果判定是 4 字节编码的高 16 位,那么紧挨着的 2 个字节肯定是这个 4 字节编码的低 16 位,顺序读取即可,不需要再做判断

2.3.3、UTF-8

相比 UTF-16 ,字符对字符占用存储空间的大小,控制得更加精细,编码也更加复杂,它同样使用变长编码
包括 4 种类型的编码:1 字节编码、2 字节编码、3 字节编码、4 字节编码,不同范围内的编号使用不同的编码

  • U+0000 ~ U+007F 范围内的编号使用 1 字节编码
  • U+0080 ~ U+07FF 范围内的编号使用 2 字节编码
  • U+0800 ~ U+FFFF 范围内的编号使用 3 字节编码
  • U+DC00 ~ U+DFFF 范围内的编号使用 4 字节编码

具体的编码规则如下所示

编码 范围 第 1 个字节 第 2 个字节 第 3 个字节 第 4 个字节
1 字节编码 0000~007F 0xxxxxxx
2 字节编码 0080~07FF 110xxxxx 10xxxxxx
3 字节编码 0800~FFFF 1110xxxx 10xxxxxx 10xxxxxx
4 字节编码 10000~10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

在 UTF-8 的编码规则中
1 字节编码的首字节的前缀为 0
2 字节编码的首字节的前缀为 110
3 字节编码的首字节的前缀为 1110
4 字节编码的首字节的前缀为 11110
尾随字节的前缀均为 10,尾随字节并没有继续区分是哪种编码的尾随字节

我们再来看,上图中的 xxxx 如何替换为具体的 Unicode 编号,我们举一个例子来分析
在 U+0080 ~ U+07FF 范围内的编号,最多只需要 11 位二进制位来表示
我们将 11 位二进制位的前 5 位放入 2 字节编码的第一个字节的 xxxxx 中,把后 6 位放入第二个字节的 xxxxxx 中
image

跟 UTF-16 编码类似, UTF-8 这样编码的目的是,明确读取出来的字节,属于哪种类型的编码(1 字节编码、2 字节编码 ...)
因为 UTF-8 的最短编码长度是 1 字节,在读取二进制文件进行解码时,我们每次读取一个字节,判定是哪种类型的首字节编码
假如是 3 字节编码的首字节编码,那么我们再顺序往下读取 2 个尾随字节

2.3.4、UTF 编码比较

UTF-8 比 UTF-16 采用更加复杂的编码,那么,在平时的开发中,使用 UTF-8 是不是一定比使用 UTF-16 更加节省存储空间呢?

答案是否定的

  • 仔细观察编号范围与编码长度,我们可以发现,如果在开发中,存储英文字符居多,那么,使用 UTF-8 更加节省空间
    因为为了兼容 ASCII 码,Unicode 中编号 0 ~ 127 之间的字符跟 ASCII 码一一对应
    英文字符的 Unicode 编号在 0 ~ 127 之间,使用 UTF-8 编码只需要 1 个字节长度,而使用 UTF-16 编码则需要 2 个字节长度
  • 但是,如果存储非英文字符居多,比如中文,那么使用 UTF-16 反倒会更加节省空间
    因为常用的非英文字符,在 UTF-16 中编码长度为 2 字节,而在 UTF-8 中编码长度为 2 字节或 3 字节,并且 3 字节居多

总结:英文多用 UTF-8,中文多用 UTF-16

  • UTF-8:英文 1 字节,中文 2 ~ 3 字节(3 字节居多)
  • UTF-16:英文 2 字节,中文 2 字节
  • UTF-32:英文 4 字节,中文 4 字节

3、Java 中 char 的字符编码

因为 C 语言出现的较早,彼时多数计算机还只支持英文系统,而 C++ 又继承了 C 语言的特性
所以,C / C++ 中的 char 类型占用一个字节长度,只能存储 ASCII 字符,完全满足英文系统的编程开发
在此之后,随着计算机到世界各地,C / C++ 选择使用 char 数组(char[])来存储非 ASCII 字符,比如中文

因为 Java 出现较晚,Unicode 已经流行
为了让 char 类型表示更多的字符,Java 设计了两个字节长的 char 类型,存储部分 Unicode 字符(U+0 ~ U+FFFF之间的),Unicode 字符会通过 UTF-16 编码之后存储到 char 类型变量中

Java 中的 char 类型只占 2 个字节长度,所以,并不能存储所有的 UTF-16 编码,也就不能表示所有的 Unicode 字符
不过,平时经常用到的字符,一般都是 Unicode 编号处于 U+0 ~ U+FFFF 之间的字符
为了避免存储空间的浪费,Java 让 char 类型占 2 字节长度,只表示 Unicode 编号处于 U+0 ~ U+FFFF之间的字符
跟 ASCII 码类似,我们也有 3 种方法将 Unicode 字符赋值给 char 类型变量

  • 对于可显示字符,我们可以直接使用字符
  • 对于所有字符(可显示或不可显示),我们都可以将字符对应的 UTF-16 编码表示为 \uxxxx 的形式赋值给变量,其中 xxxx 为 16 进制
  • 对于所有字符(可显示或不可显示),我们都可以将字符对应的 Unicode 编号赋值给变量
char a = '我'; // 字符本身
char b = '\u6211'; // UTF-16 编码
char c = 0x6211; // Unicode 编号
System.out.println(a); // 打印: 我
System.out.println(b); // 打印: 我
System.out.println(c); // 打印: 我

那么,U+10000 ~ U+10FFFF 范围内的 Unicode 字符在 Java 中如何存储呢?
类似 C / C++ 存储 ASCII 码之外字符的做法,Java 使用 char 数组来存储 U+10000 ~ U+10FFFFF 之间的字符,示例代码如下所示

// 🜁 这个字符的 Unicode 编号为 U+1F701, UTF-16 编码为: D83D DF01
char[] chs = new char[2];
chs[0] = '\uD83D';
chs[1] = '\uDF01';
System.out.println(chs); // 🜁
String s = "\uD83D\uDF01";
System.out.println(s); // 🜁
char[] chs2 = Character.toChars(0x1F701);
System.out.println(chs2); // 🜁

4、课后思考题

1、在本节中,我们编写代码,实现了将字符串 "231" 转化成整数 231,那么,请你编写代码,实现将整数 231 转换成字符串 "231"

public String convert(int val) {
char[] chars = new char[32];
int i = 0;
while (val != 0) {
chars[i++] = (char) ('0' + val % 10);
val /= 10;
}
for (int j = 0; j < i / 2; j++) {
char tmp = chars[j];
chars[j] = chars[i - 1 - j];
chars[i - 1 - j] = tmp;
}
return new String(chars, 0, i);
}

2、将一个只包含 a ~ z、A ~ Z、0 ~ 9 之间字符的字符串转换为小写字符串,例如,"A34bc" 转化为 "a34bc")

public String convert(String str) {
char[] res = new char[str.length()];
for (int i = 0; i < str.length(); ++i) {
char c = str.charAt(i);
if (c >= 'A' && c <= 'Z') res[i] = (char) (c + 'a' - 'A');
else res[i] = str.charAt(i);
}
return new String(res);
}
posted @   lidongdongdong~  阅读(94)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开