7、字符
字符、字符串是编程语言中必不可少的语法,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
因为 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 中
跟 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); }
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17394037.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步