JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统Android面试Java

Java | 详解 Unicode 字符集

2020-06-15  本文已影响0人  彭旭锐

前言

字符编码和Unicode字符集在日常开发中并不是很常用,但在阅读源码或文章时也偶尔会碰到相关的知识点总是一知半解,来一招过!


目录


1. 字符编码简介

# 咬文嚼字 #

很多名词都可以带上“编码”两个字,容易混淆。这里列举出“编码”的三层含义,以后在文章中看到“编码”再结合上下文即可理解作者的意思。

  • 作为动词,表示把一个字符转换为一个二进制机器数的过程,这个机器数才是字符在计算机中真实存储/传输的格式。例如把 A 转换为65(ASCII)的动作,就是编码;
  • 作为名词,可以表示字符转换为机器数之后的那个值,对于 A 来说,65(ASCII)就是 A 的编码(值),有时会称为编号;
  • 作为名词,可以表示把字符转换为机器数的编码方案,例如 ASCII 编码、GBK 编码、UTF-8 编码。
常见字符集

2. Unicode 的编号规则

为了解决字符集间互不兼容的问题,包罗万象的 Unicode 字符集出场了,要点如下:


3. Unicode 的三种实现方式

Unicode的实现方式不同于编码方式。一个字符的 Unicode 编码是确定的,但是在实际存储 / 传输中,处于节省空间或运算效率的考量,使用的 Unicode 编码的实现方式有所不同。Unicode 的实现方式称为 Unicode转换格式(Unicode Transformation Format,简称为 UTF),常见的有 UTF-8、UTF-16 和 UTF-32。

3.1 UTF-32

UTF-32使用4个字节的定长编码,前面说到Unicode码点最大需要3个字节的空间,这对于4个字节UTF-32编码来说绰绰有余。

  1. 编码值是码点的原码,例如:
U+0000   => 0x00000000
U+6C38   => 0x00006C38
U+10FFFF => 0x0010FFFF

3.2 UTF-16

UTF-16是2个字节或4个字节的变长编码,结合了UTF-8UTF-32两者的特点

  1. 编号范围在U+0000 ~ U+FFFF的码点(基本平面)使用2个字节表示;
  2. 编号范围在U+10000 ~ U+10FFFF的码点(辅助平面)使用4个字节表示:
    16个辅助平面总共有2^{20}个字符,需要20bits的空间才能区分。UTF-16将这20位拆成两半,高10位映射在U+D800 ~ U+DBFF,称为高位代理(high surrogate),低10位映射在U+DC00 ~ U+DFFF,称为低位代理(low surrogate)

第一条规则比较好理解,怎么理解第二条规则呢?

我们知道辅助平面字符的范围是U+10000 ~ U+10FFFF,转换为二进制一共需要21 bits,1个char只有16位肯定是不够的,那么用2个char该如何表示呢?

最简单的方法一个char表示低16位,另一个char表示高5位(多余11位置零),如下图所示:

怎么理解前缀有歧义?假如给到一个char,它的机器数范围是0 ~ 0xFFFF,那么它既可能是基本平面字符的前缀,也可能是辅助平面字符的前缀。那么对于一串字符流(字节流同理),我们就无法区分出哪一个char应该单独解析为基本平面字符,哪一个char应该和后继的一个char合起来解析为辅助平面字符。

为了解决这个问题,必须实现前缀无歧义编码(PFC编码,类似的还有哈弗曼编码)。UTF-16的方案是将用于基本平面字符char和辅助平面字符的char的机器数范围错开,这个方案的前提就是在基本平面中有一段区域是专门空出一段区域作为UTF-16的代理:

Plane 0中,浅灰色的 D8 ~ DF 为 UTF-16 代理区 —— 引用自维基百科

具体解释如下:

下表举例了一起字符的转换过程:

UTF-16 示例 —— 引用自维基百科

Java的String的内存表示基于UTF-16 BE编码,我们可以在StringCharacter中找到相应的支持:

// String.java

public String(int[] codePoints, int offset, int count) {
    // 0. 前处理:参数不合法的情况
    final int end = offset + count;

    // 1. 计算总共需要的char数组容量
    int n = count;
    for (int i = offset; i < end; i++) {
        int c = codePoints[i];
        // 分析点 1.1
        if (Character.isBmpCodePoint(c))
            continue;
        // 分析点 1.2
        else if (Character.isValidCodePoint(c))
            n++; // 每个辅助平面字符需要多一个char
        else throw new IllegalArgumentException(Integer.toString(c));
    }

    // 2. 分配数组并填充数据
    final char[] v = new char[n];
    for (int i = offset, j = 0; i < end; i++, j++) {
        int c = codePoints[i];
        // 分析点 2.1
        if (Character.isBmpCodePoint(c))
            v[j] = (char)c;
        else
        // 分析点 2.2
        Character.toSurrogates(c, v, j++);
    }
    // 结束
    this.value = v;
}

// Character.java

// 分析点 1.1:判断码点是否处于基本平面
public static boolean isBmpCodePoint(int codePoint) {
    return codePoint >>> 16 == 0;
}
// 分析点 1.2:判断码点是否处于辅助平面
public static boolean isValidCodePoint(int codePoint) {
    int plane = codePoint >>> 16;
    return plane < ((0x10FFFF + 1) >>> 16);
}
// 分析点 2.2:辅助平面字符 - 规则2
static void toSurrogates(int codePoint, char[] dst, int index) {
    // high在高位,low在低位,是大端序
    dst[index+1] = lowSurrogate(codePoint);
    dst[index] = highSurrogate(codePoint);
}
// 计算高位代理
public static char highSurrogate(int codePoint) {
    return (char) ((codePoint >>> 10) + (0xDBFF - (0x010000 >>> 10)));
}
// 计算低位代理
public static char lowSurrogate(int codePoint) {
    return (char) ((codePoint & 0x3ff) + 0xDC00);
}

反过来,从UTF-16编码解码出codepoint的代码:

// Character.java
public static int toCodePoint(char high, char low) {
    // 源码有算术表达式优化,此处为等价逻辑
    return ((high - 0xD800) << 10) + (low - 0xDC00) + 0x010000;
}

3.3 UTF-8

UTF-8是1~4个字节的变长编码

下述规则表述与你在任何文章 / 百科里看到的规则表述不一样,但是逻辑上是一样的。因为笔者更倾向于使用前缀无歧义的概念理解UTF-8的编码规则。

    1. 不同范围的码点值使用不同长度的编码
    1. 1字节编码前缀为0、2字节编码前缀为110、3字节编码前缀为1110、4字节编码前缀为11110
    1. 每个码元的非首字节前缀为10
      Unicode 和 UTF-8 之间的转换关系表 ( x 字符表示码点占据的位 ) 引用自维基百科

UTF-8是常用的Unicode编码方式,很多地方都会发现它的身影,例如:

类型 标示 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_String_info 8 字符串类型字面量

其中CONSTANT_Utf8_info常量的结构:

类型 名称 数量
u1 tag 1
u2 length 1
u1 bytes length

可以看到,Class文件中的字符串只支持基本平面字符,同时length的值说明UTF-8编码的字符串常量的字节数,u2能表达的最大值是65535,所以Java中定义的变量名和方法名超过64KB将无法通过编译。

3.4 小结

ASCII UTF-8 UTF-16 UTF-32 UCS-2
编号空间 0-7F 0-10FFFF 0-10FFFF 0-10FFFF 0-FFFF
最少字节 1 1 2 4 2
最多字字节 1 4 4 4 2
字节序 \times \times \checkmark \checkmark \checkmark

参考资料


推荐阅读

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的简书!

上一篇 下一篇

猜你喜欢

热点阅读