【Java】 字符(串)编码与解码

Posted by 西维蜀黍 on 2019-03-02, Last Modified on 2021-09-21

背景

究竟什么是字符?

众所周知,一个字符(character)就是一个字((letter),一串字母组成一个单词,一组单词组成句子,以此类推。然而,事实上,在计算机中,在屏幕上描述的字符(被称为字符的图符),和为这个字符指定的数值(被称为代码值),并不是直接对应的。

在 ASCII 中,定义了96个可印刷的字符,就可以用来书写英语。这与定义了2万多个图符还不足以表述其所有文字的中文相比,简直是天差地别。从早期的摩尔斯码和波多码开始,英语整体的简单性(较少的图符,按统计频率出现)就使其成为了数字化时代的一门通用语言。但是随着更多的人进入到数字化时代,随着非英语国家更多地使用计算机,越来越多的人们遂渐不能容忍计算机只能用ASCII码和只能表述英语。这极大地增加了计算机能够理解的“字符”的数量。由此,人们意识到,计算机所用的字符编码位数必须翻倍。

当受人尊敬的7位ASCII码被合并成为8位的被称为ISOlatin-1(或ISO8859_1),ISO表示国际标准化组织)字符编码之后,可利用的字符数量翻了一倍。正如你可能从这一编码的名字中想到的一样,这个标准保证了在计算机上,许多的欧洲国家可以描述它们的语言。然而,仅仅是标准的确立,还不能意味着标准的利用。那时,许多的计算机产商为了某些利益,已经开始利用了8位字符中的其它128个“字符”。目前还能看到的利用这些额外字符的例子有IBM的个人计算机(PC),和曾一度最为流行的计算机终端,DEC公司的VT-100。后者在终端仿真软件上继续存在。

究竟何时停止八位字符的使用,这一问题将会在以后的数十年内一直争论下去,但是何时提出这一问题却可以做出回答。我认为,从1984年引入Macintosh计算机时起,这一问题就已开始提出。Macintosh为主流计算机引入了两个革命性的概念:存储于RAM中的字符字体;和可以描述所有语言所用字符的世界文字体系(WorldScript)。当然,这其实也只是Xerox公司所做所为的一个翻版,在它的蒲公英(Dandelion)系列机器上,就以Star字处理系统的形式,利用了这些技术。然而,是Macintosh把这些新的字符集和字体带给了还在利用“哑”终端的用户。一旦有人开了头,利用不同字体的做法就无法被终断──因为许许多多的人们对其爱不释手。到80年代后期,为了合理而标准地使用这些字符,一个名为Unicode协会(UnicodeConsortium)的组织应运而生,并于1990年,发布了它的第一个Unicode规范。然而不幸的是,在80年代甚至在进入90年代之后,字符集的数量在成倍的增长,在那时,几乎正在从事新字符编码的所有工程师,都认为刚刚上步的Unicode标准不会长久,因而,他们为各种文字,创建了与他人各不相同的编码。然而,即使是在Unicode不被广泛采纳的情况下,那种认为只有128个或最多256个字符能被采用的观念已不复存在。在Macintosh之后,对不同字体的支持已成为字处理系统中不可缺少的功能。八位字符正在消褪,遂渐消亡。

如何“翻译”

明白了各种语言需要交流,经过翻译是必要的,那又如何来翻译呢?计算中提拱了多种翻译方式,常见的有 ASCII、ISO-8859-1、GB2312、GBK、UTF-8、UTF-16 等。它们都可以被看作为字典,它们规定了转化的规则,按照这个规则就可以让计算机正确的表示我们的字符。目前的编码格式很多,例如 GB2312、GBK、UTF-8、UTF-16 这几种格式都可以表示一个汉字,那我们到底选择哪种编码格式来存储汉字呢?这就要考虑到其它因素了,是存储空间重要还是编码的效率重要。根据这些因素来正确选择编码格式,下面简要介绍一下这几种编码格式。

  • ASCII 码

学过计算机的人都知道 ASCII 码,总共有 128 个,用一个字节的低 7 位表示,0~31 是控制字符如换行回车删除等;32~126 是打印字符,可以通过键盘输入并且能够显示出来。

  • ISO-8859-1

128 个字符显然是不够用的,于是 ISO 组织在 ASCII 码基础上又制定了一些列标准用来扩展 ASCII 编码,它们是 ISO-8859-1~ISO-8859-15,其中 ISO-8859-1 涵盖了大多数西欧语言字符,所有应用的最广泛。ISO-8859-1 仍然是单字节编码,它总共能表示 256 个字符。

  • GB2312

它的全称是《信息交换用汉字编码字符集 基本集》,它是双字节编码,总的编码范围是 A1-F7,其中从 A1-A9 是符号区,总共包含 682 个符号,从 B0-F7 是汉字区,包含 6763 个汉字。

  • GBK

全称叫《汉字内码扩展规范》,是国家技术监督局为 wWndows95 所制定的新的汉字内码规范,它的出现是为了扩展 GB2312,加入更多的汉字,它的编码范围是 8140~FEFE(去掉 XX7F)总共有 23940 个码位,它能表示 21003 个汉字,它的编码是和 GB2312 兼容的,也就是说用 GB2312 编码的汉字可以用 GBK 来解码,并且不会有乱码。

  • GB18030

全称是《信息交换用汉字编码字符集》,是我国的强制标准,它可能是单字节、双字节或者四字节编码,它的编码与 GB2312 编码兼容,这个虽然是国家标准,但是实际应用系统中使用的并不广泛。

  • UTF-16

说到 UTF 必须要提到 Unicode(Universal Code 统一码),ISO 试图想创建一个全新的超语言字典,世界上所有的语言都可以通过这本字典来相互翻译。可想而知这个字典是多么的复杂,关于 Unicode 的详细规范可以参考相应文档。Unicode 是 Java 和 XML 的基础,下面详细介绍 Unicode 在计算机中的存储形式。

UTF-16 具体定义了 Unicode 字符在计算机中存取方法。UTF-16 用两个字节来表示 Unicode 转化格式,这个是定长的表示方法,不论什么字符都可以用两个字节表示,两个字节是 16 个 bit,所以叫 UTF-16。UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是 Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。

  • UTF-8

UTF-16 统一采用两个字节表示一个字符,虽然在表示上非常简单方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的今天,这样会增大网络传输的流量,而且也没必要。而 UTF-8 采用了一种变长技术,每个编码区域有不同的字码长度。不同类型的字符可以是由 1~6 个字节组成

UTF-8 有以下编码规则:

  1. 如果一个字节,最高位(第 8 位)为 0,表示这是一个 ASCII 字符(00 - 7F)。可见,所有 ASCII 编码已经是 UTF-8 了。
  2. 如果一个字节,以 11 开头,连续的 1 的个数暗示这个字符的字节数,例如:110xxxxx 代表它是双字节 UTF-8 字符的首字节。
  3. 如果一个字节,以 10 开始,表示它不是首字节,需要向前查找才能得到当前字符的首字节

char 的学习

由于 Java 采用的是 16 位的 Unicode 字符集,即 UTF-16,所以在 Java 中 char 数据类型是定长的,其长度永远只有 16 位(2个字节),这里的定长是与 UTF-8 进行区别的,因为 UTF-8 使用变长机制来表示字符。

char 数据类型永远只能表示代码点在 U+0000 ~ U+FFFF 之间的字符:

  • 最小值是 \u0000(即为0);
  • 最大值是 \uffff(即为65,535);

如果代码点超过了这个范围,即使用了增补字符,那么 char 数据类型将无法支持,因为增补字符需要 32 位的长度来存储,我们只能转而使用 String 来存储这个字符。

Java 中使用 Unicode

Java 中使用 Unicode 的原因是,Java 的 Applet 允许全世界范围内运行,那它就需要一种可以表述人类所有语言的字符编码。

而当只表示 English,Spanish,German 或 French等语言时,根本不需要 2 个字节的长度这么长来表示,所以这时其实采用ASCII码会更高效。

因此,是采用 Unicode 还是 ASCII 来编码一个字符就存在一个权衡问题,即是获得更全的字符表达范围,还是使用更少的存储空间。

字符常量有三种表示形式

单个字符

直接通过单个字符来指定字符常量:例如,‘A’、‘a’、‘8’、“中”等。

//直接指定单个字符的字符常量
char aChar = 'a';

//指定一个中字符常量
char zhong = '中';
转义字符

通过转义字符表示特殊的字符常量:例如:’\n’、’\t’等。

有反斜杠(\)在前的字符是一个转义序列并且对于编译器有特殊的意义。

换行符(\n)在 System.out.println() 语句中经常使用,在字符串打印出来后换行。

以下的表格展示了 Java 转义序列:

转义序列 描述
\t 在文本中插入一个标签。
\b 在文本中插入一个退格。
\n 在文本中插入一个换行符。
\r 在文本中插入一个回车。
\f 在文本中插入一个换页。
' 在文本中插入一个单引号字符。
\ 在文本中插入一个反斜杠字符。
Unicode 值

直接使用 Unicode 值来表示字符常量。即使用一个十进制数,八进制数或十六进制数的整数来表示一个字符的 Unicode 值。

\u 在 Java 中表示这是一个十六进制数。

当用一个十六进制整数赋值给 一个 char 时,格式是 ‘\uXXXX’,其中XXXX代表一个十六进制的整数,范围是:’\u0000’—-’\uFFFF’,一共可以表示65536个字符,其中前256个字符(’\u0000’—’\u00FF’)和ASCII码中的字符完全重合。

//使用十六进制的字符的Unicode编码值来赋值
char ch1 = '\u9999';

int zhongValue=zhong;
//用一个表示十进制数整数来表示Unicode编码值以赋值
char ch2 = (char) zhongValue;
//将会打印出“中”
System.out.println(ch2);

// “中”的 Unicode 值为 20013 (十进制)
char ch3 = 20013;
//将会打印出“中”
System.out.println(ch3);

Java 中如何编解码

Charset 提供 encode 与 decode 分别对应 string 到 byte[] 的编码和 byte[] 到 char[] 的解码。如下代码所示:

Charset charset = Charset.forName("UTF-8"); 
ByteBuffer byteBuffer = charset.encode(string); 
CharBuffer charBuffer = charset.decode(byteBuffer);

Reference