详谈 Java 中的字符编码

概述

Java 语言内部使用的是 Unicode 字符集,存储一个字符时,支持 UTF-16 和 LATIN1 两种编码方式。但其实,Java 内部还实现了 ASCII、UTF-8 编码,可以很容易实现这些编码之间的相互转换。对于基本类型 char,使用的是 UTF-16 编码,一个 char 类型的变量占两个字节。而对于 String 类型,如果串中的字符均为 ASCII 字符集中的字符,则默认使用 LATIN1 方式编码,否则,使用 UTF-16。由于计算机中 ASCII 字符使用更频繁,这样做可以节省大量空间。

Unicode

Unicode 是一种现今普遍使用的编码规范。简而言之,Unicode 规定了世界上几乎所有字符的二进制编码,解决了计算机内字符编码统一的难题。Unicode 将所有字符分成多个平面,每个平面的编码长度为 16 位,故每个平面可以编码 65536 个字符。最常用的平面是 Basic Multilingual Plane(基本多文种平面),简称 BMP。它又被称为 “零号平面”,plane 0。 BMP 平面编码范围为 U+0000 至 U+FFFF,基本囊括了各国语言符号。

CodePoint

所谓代码点,其实就是 Unicode 字符集中任意字符的编码对应的整数形式。如 ‘A’ 的编码为 U+0041,其代码点为 65。我们如雷贯耳的 ASCII 码其实指的就是代码点。Java 中的 int 类型占四个字节,足以表示 Unicode 字符集中任意字符的代码点。而代码点又可以转换成各种实际的二进制编码形式,所有 Java 中很多关于字符串处理的方法都支持传入一个 int 类型的数组,用一系列代码点来表示一串字符。

LATIN 1

LATIN 1 既可以作为一种编码规范,也可以作为一种计算机存储实际使用的编码方式,它是 ASCII 编码的一种扩展。ASCII 字符集只使用 7 位二进制编码,共可编码 128 个字符。而 LANTIN 1 使用 8 位二进制编码,刚好可以用一个字节存储。代码点 0 ~ 127 代表的字符和 ASCII 一致,128 ~ 255 用来表示卡丁字母表中的特殊字符。Java 是支持 LATIN 1 的,甚至 String 类型默认使用的就是 LATIN 1,除非字符串中包含 LATIN1 不可编码的字符。因为 String 中字符串编码其实是存储在一个 byte 类型的数组中,使用 LATIN1 的话数组中每一个 byte 均表示一个字符,不仅编解码方便,还大大节省了空间。幸运的是,Unicode 其实也算是 LATIN1 的扩展,如果将 LATIN1 字符集中的字符用 UTF-16 来编码,那么低 8 位就是 LATIN1 编码,高 8 位是全 0。故 Unicode 和 LANTIN1 之间的转换也十分方便。

UTF-16

Unicode 是一种编码规范,而 UTF-16 则是计算机存储字符时实际使用的编码方式。其实很容易理解,Unicode 规范解决的是字符到代码点的映射关系,而 UTF-16 解决的是怎么将代码点实际存储到计算机中。UTF-16 使用定长的两个字节来存储一个 Unicode 字符,而 Java 中的基本类型 char 正好占用两个字节,使用的就是 UTF-16 来编码。但是,Unicode 的编码范围是 U+0000 至 U+10FFFF,一个 char 是不可能表示 Unicode 字符集中所有字符的。所以又引入了 supplementary characters 的概念。所谓 supplementary characters 指的就是不属于 BMP 的字符,将它们用一对 char 来表示。第一个 char 称为 high-surrogates,编码范围为 U+D800 - U+DBFF。第二个 char 称为 low-surrogates,编码范围为 U+DC00~U+DFFF。使用它们的组合来存储一个非 BMP 字符。

Code Unit

代码单元指的是某种编码方式的最小处理单位。例如,LATIN1 编码的代码单元为一个字节,而 UTF-16 编码的代码单元则为两个字节。对于 UTF-16 编码,存储一个 supplementary characters,需要两个代码单元。

UTF-8

UTF-8 是计算机存储 Unicode 字符时的另一种编码方式,它是一种可变长度字符编码,即其代码单元是不固定的。UTF-8 根据一个字节的前几位来判断这个字符是用几个字节来表示的。例如,如果某个字节是以 0 开头的,即 0xxxxxxx。这么这个字节表示的就是 ASCII 字符,代码单元为一个字节。而如果是以 110 开头,则该字符要用两个字节来表示,且格式为 110xxxxx 10xxxxxx。如果以 1110 开头,则该字符需要用三个字节表示,格式为 1110xxxx,10xxxxxx,10xxxxxx。一般,在 UTF-8 中,汉字是用三字节表示,英文字符用单字节。用单字节表示英文字符时,编码结果和 ASCII 编码一致。而用三字节表示一个汉字时,其实就是将汉字在 Unicode 字符集中的代码点填充到三字节格式的待定位 x 中。需要提醒的是,java 使用的其实不是标准 UTF-8,而是支持用于字符串串行化的修正 UTF-8。

UTF-8 和 UTF-16 比较

UTF-8 和 UTF-16 是 Unicode 字符串最为常用的两种编码方式。还有 UTF-32,它也是一种定长的编码方式,代码单位为 4 字节,原理和 UTF-16 类似,故不再赘述。在比较 UTF-8 和 UTF-16 时,我们先要搞清楚在打开一个 Unicode 字符串文本时,怎么确定该文本的编码方式呢?其实,每个文本开头会设置一些标志位来标识文本的编码方式以及大小端。下面列举出这些标志位及其代表的编码方式:

标志位编码方式大端/小端
EF BB BFUTF-8
FE FFUTF-16/UCS-2小端
FF FEUTF-16/UCS-2大端
FF FE 00 00UTF-32/UCS-4小端
00 00 FE FFUTF-32/UCS-4大端

下面做一个简单的实验,将一个 String 转换成 UTF-16 编码的 bytes[],看看会发生什么:

        String s = "A";
        byte[] bytes = s.getBytes("UTF16");
        System.out.println(bytes.length);
        System.out.printf("%x %x",bytes[0], bytes[1]);

out:
4
fe ff

可以看到,“A” 以 UTF-16 进行编码,理论上只占两个个字节,而转换成的 byte 数组却存储了四个字节。打印 byte 数组中的前两个字节,发现内容是 “fe ff”,可见编码时自动在开头加入了 UTF-16 大端编码的标志位。
UTF-8 和 UTF-16 在使用上各有优势和缺点。
UTF-8 一个显著的优势便是每个字节都有标识字节类型的标志位,因此字符边界很容易确定。这就使得 UTF-8 局部字节的错误不会导致连锁反应,容错率很高。但伴随而来的,在执行解码、编码转换、索引查找等操作时必然要付出更大的开销。故 UTF-8 编码的可靠性很适合网络交换、磁盘存储等场景使用,但在程序内部需要对字符串进行各种处理时,UTF-8 会影响性能。
UTF-16 的优势所在便是其代码单元是固定的,这就使得它对字符串处理的效率很高,很适合在程序内部使用。但问题便是字节中没有标识字节含义的标志位,这就使得局部字节出错会影响后续字节,容错率低。除此之外,UTF-16 存在大小端字节序的问题(UTF-8 不存在大小端问题,因为每个字节都有标志位,所以不需要靠字节顺序来判断字节含义),而大小端转换必然要付出额外开销。故而,在网络交换等需要跨平台以及可靠性的场景下,UTF-16 并不适用。
在空间上,其实 UTF-8 及 UTF-16 各有优势。对于 ASCII 字符集中的字符,UTF-8 仅需要一个字节,和 LATIN1 一样。故大量英文单词的场景中,UTF-8 十分节省空间。但是,对于汉字而言,UTF-8 却需要三字节,而 UTF-16 只要两字节。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值