编码那些事儿

263 阅读12分钟

编码信息从一种形式格式转换为另一种形式的过程;解码则是编码的逆过程。

问题:计算机中存储只有0和1,现实世界有字母、数字、各种语言等,0和1如何编码?

  • 计算机中存储信息的最小单元是一个字节即8个bit,28所以能表示的字符范围是 0-255个,2个字节0-65535
  • Java中byte(1字节) <-> char(2字节),String的value在jdk8中char[],jdk17中使用byte[]

ASCII

在计算机中,所有的数据在存储和运算时都要使用二进制数表示。例如,像a、b、c、d这样的52个字母(包括大写)以及0、1等数字还有一些常用的符号(例如*、#、@等)在计算机中存储时也要使用二进制数来表示,而具体用哪些二进制数字表示哪个符号,这就是编码。如果不同的计算机要想互相通信而不造成混乱,那么每台计算机就必须使用相同的编码规则,于是美国有关的标准化组织就推出了ASCII编码。

ASCII是由美国国家标准学会(American National Standard Institute,ANSI)制定的,使用标准的单字节字符编码方案,用于基于文本的数据。方案起始于50年代后期,在1967年定案。它最初是美国的标准,供不同计算机在相互通信时需共同遵守的西文字符编码标准。现已被国际标准化组织(International Organization for Standardization,ISO)定为国际标准(ISO/IEC 646),适用于所有拉丁字母。

共128个字符,只用了7位

  • 控制字符的编号范围是0-31和127(0x00-0x1F和0x7F),共33个字符
  • 可显示字符编号范围是32-126(0x20-0x7E),共95个字符

ASCII的局限在于只能显示26个基本拉丁字母、阿拉伯数字和英式标点符号,因此只能用于显示现代美国英语(且处理naïve、café、élite等外来语时,必须去除附加符号)。虽然EASCII解决了部分西欧语言的显示问题,但对更多其他语言依然无能为力。因此,现在的软件系统大多采用Unicode,特别是与ASCII向下兼容的UTF-8

ascii.org.cn/

Unicode

超语言字典,世界上所有的语言都可以通过这本字典来相互翻译

Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符

目前的Unicode字符分为17组编排,每组称为平面(Plane),而每平面拥有65536(即216)个代码点。然而目前只用了少数平面。

平面始末字符值中文名称英文名称
0号平面U+0000 - U+FFFF基本多文种平面Basic Multilingual Plane,简称BMP
1号平面U+10000 - U+1FFFF多文种补充平面Supplementary Multilingual Plane,简称SMP
2号平面U+20000 - U+2FFFF表意文字补充平面Supplementary Ideographic Plane,简称SIP
3号平面U+30000 - U+3FFFF表意文字第三平面Tertiary Ideographic Plane,简称TIP
4号平面 至 13号平面U+40000 - U+DFFFF(尚未使用)
14号平面U+E0000 - U+EFFFF特别用途补充平面Supplementary Special-purpose Plane,简称SSP
15号平面U+F0000 - U+FFFFF保留作为私人使用区(A区) [1]Private Use Area-A,简称PUA-A
16号平面U+100000 - U+10FFFF保留作为私人使用区(B区) [1]Private Use Area-B,简称PUA-B

Unicode的编码空间可以划分为17个平面(plane),每个平面包含65,536个码位。

17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从0016到1016,共计17个平面。

  • 第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)
    • 从U+D800到U+DFFF之间的码位区段是永久保留不映射到Unicode字符。(UTF-16就利用保留下来的0xD800-0xDFFF区块的码位来对辅助平面的字符的码位进行编码。)
  • 其他平面称为辅助平面(Supplementary Planes)。

unicode-table.com/cn/

UTF-8

UTF-88-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用一至四个字节对Unicode字符集中的所有有效编码点进行编码,属于Unicode标准的一部分,最初由肯·汤普逊罗布·派克提出

Unicode 和 UTF-8 之间的转换关系表 ( x 字符表示码点占据的位 )

码点的位数码点起值码点终值字节序列Byte 1Byte 2Byte 3Byte 4Byte 5Byte 6
  7U+0000U+007F10xxxxxxx
11U+0080U+07FF2110xxxxx10xxxxxx
16U+0800U+FFFF31110xxxx10xxxxxx10xxxxxx
21U+10000U+1FFFFF411110xxx10xxxxxx10xxxxxx10xxxxxx
26U+200000U+3FFFFFF5111110xx10xxxxxx10xxxxxx10xxxxxx10xxxxxx
31U+4000000U+7FFFFFFF61111110x10xxxxxx10xxxxxx10xxxxxx10xxxxxx10xxxxxx

UTF-16

定长的表示方法,最小为16位一个单元,-16的意思也即两字节为一个单元

当然,定长的问题主要是空间的浪费、传输成本的增加等;

UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作,这也是Java以其作为内存的字符存储格式的一个很重要的原因。

Java 的char用两字节存储,表示范围从 '\u0000' 到 '\uffff' ,也就是从0到65535。 事实上,一个char不能表示65535个字符

  • 因为只有U+0000到U+D7FF和U+E000到U+FFFF能用来表示一个完整的字符,这些叫做 BMP(Basic Multilingual Plane)
  • 另外的作为high surrogate和 low surrogate拼接组成由4字节表示的字符。(U+D800到U+DFFF之间的码位区段

在UTF-16编码中,大于U+10000码位将被编码为一对16比特长的码元,即按4个字节编码,2个编码单元,此时单个char无法表示。

下表是Unicode编码对应UTF-16编码格式

Unicode编码范围(16进制)具体Unicode码(二进制)UTF-16编码方式(二进制)字节
0000 0000 - 0000 FFFFxxxxxxxx xxxxxxxxxxxxxxxx xxxxxxxx2
0001 0000 - 0010 FFFFyy yyyyyyyy xx xxxxxxxx110110yy yyyyyyyy 110111xx xxxxxxxx4

具体的编码规则如下:

  • 对于 Unicode 码小于 0x10000 的字符, 使用 2 个字节存储,并且是直接存储 Unicode 码,不用进行编码转换
  • 对于 Unicode 码在 0x10000 和 0x10FFFF 之间的字符,使用 4 个字节存储,这 4 个字节分成前后两部分,每个部分各两个字节,其中,前面两个字节的前 6 位二进制固定为 110110,后面两个字节的前 6 位二进制固定为 110111, 前后部分各剩余 10 位二进制表示符号的 Unicode 码 减去 0x10000 的结果
  • 大于 0x10FFFF 的 Unicode 码无法用 UTF-16 编码

以U+10437编码(𐐷)为例:

  1. 0x10437 减去 0x10000,结果为0x00437,二进制为 0000 0000 0100 0011 0111
  2. 分割它的上10位值和下10位值(使用二进制):0000 0000 01 和 00 0011 0111
  3. 添加 0xD800 到上值,以形成高位:0xD800 + 0x0001 = 0xD801
  4. 添加 0xDC00 到下值,以形成低位:0xDC00 + 0x0037 = 0xDC37

下表总结了一起示例的转换过程,颜色指示码点位如何分布在所述的UTF-16中。由UTF-16编码过程中加入附加位的以黑色显示。

字符普通二进制UTF-16二进制UTF-16 十六进制 字符代码UTF-16BE 十六进制字节UTF-16LE 十六进制字节
$U+00240000 0000 0010 01000000 0000 0010 0100002400 2424 00
U+20AC0010 0000 1010 11000010 0000 1010 110020AC20 ACAC 20
𐐷U+104370001 0000 0100 0011 01111101 1000 0000 0001 1101 1100 0011 0111D801 DC37D8 01 DC 3701 D8 37 DC
𤭢U+24B620010 0100 1011 0110 00101101 1000 0101 0010 1101 1111 0110 0010D852 DF62D8 52 DF 6252 D8 62 DF

举例:

/**
 * Unicode
 * <em>UTF-8、UTF-16、UTF-32</em>
 * <em>utf8mb4,emoji</em>
 * <em>char,toHexString</em>
 *
 */
public class EncodingTest {
    public static void main(String[] args) throws UnsupportedEncodingException, CharacterCodingException {

        System.out.println("默认编码,Charset.defaultCharset().name():" + Charset.defaultCharset().name());
        // 代码源文件是按照 UTF-8 编码
        String s1 = "你好";
        String s2 = "ab";
        // String.length() 方法仅仅是简单返回 value[] 数组的长度,value[]才真正用来保存字符串的内容,
        // 字符串中的 Unicode 代码单元的数目
        // 你,4F60,好,597D,都小于U+FFFF,都使用1个单元,长度为2
        System.out.println("s1.length():" + s1.length());
        System.out.println("s2.length():" + s2.length());
        System.out.println("s1.codePointCount():" + s1.codePointCount(0, s1.length()));
        System.out.println("s2.codePointCount():" + s2.codePointCount(0, s2.length()));
        // 🎵,U+1F3B5
        System.out.println("🎵.codePointCount():" + "🎵".codePointCount(0, "🎵".length()));

        //
        System.out.println("s1.toCharArray().length:" + s1.toCharArray().length);
        System.out.println("s2.toCharArray().length:" + s2.toCharArray().length);

        System.out.println("s1.getBytes()" + Arrays.toString(s1.getBytes()));
        System.out.println("s1.getBytes().length:" + s1.getBytes().length);
        System.out.println("s1.getBytes("UTF-8").length:" + s1.getBytes(StandardCharsets.UTF_8).length);

        System.out.println("s1.getBytes("GBK"):" + Arrays.toString(s1.getBytes("GBK")));
        System.out.println("s1.getBytes("GBK").length:" + s1.getBytes("GBK").length);

        System.out.println("s2.getBytes()" + Arrays.toString(s2.getBytes()));
        System.out.println("s2.getBytes().length:" + s2.getBytes().length);
        System.out.println("s2.getBytes("UTF-8").length:" + s2.getBytes(StandardCharsets.UTF_8).length);
        System.out.println("s2.getBytes("GBK").length:" + s2.getBytes("GBK").length);

        // 4F60 597D
        System.out.println("charsToHex(s1.toCharArray()):" + charsToHex(s1.toCharArray()));
        // 6162(实际上应该是 00610062,这里省略了前导0)
        System.out.println("charsToHex(s2.toCharArray()):" + charsToHex(s2.toCharArray()));

        System.out.println("\u4F60\u597D");
        System.out.println("\u0061\u0062");
        System.out.println("-------------------------------");
        System.out.println("bytesToHex(s1.getBytes()):" + bytesToHex(s1.getBytes()));
        System.out.println("bytesToHex(s1.getBytes("UTF-8")):" + bytesToHexFormat(s1.getBytes(StandardCharsets.UTF_8)));
        System.out.println("bytesToHex(s1.getBytes("GBK")):" + bytesToHex(s1.getBytes("GBK")));
        System.out.println("-------------------------------");
        System.out.println("bytesToHex(s2.getBytes()):" + bytesToHex(s2.getBytes()));
        System.out.println("bytesToHex(s2.getBytes("UTF-8")):" + bytesToHex(s2.getBytes(StandardCharsets.UTF_8)));
        System.out.println("bytesToHex(s2.getBytes("GBK")):" + bytesToHex(s2.getBytes("GBK")));

        //根据编码创建对应的 Encoder 实例,这里是sun.nio.cs.UTF_8$Encoder
        CharsetEncoder ceUTF8 = StandardCharsets.UTF_8.newEncoder();
        //这里仅仅是举例,实际上buffer长度通过计算获取
        ByteBuffer sbUTF8 = ByteBuffer.allocate(6);
        //真正的编码转换,修改字节编码,输出到sbUTF8中。
        ceUTF8.encode(CharBuffer.wrap(s1.toCharArray()), sbUTF8, false);

        System.out.println("-------------------------------");
        System.out.println(sbUTF8.array().length);
        System.out.println(bytesToHex(sbUTF8.array()));
        System.out.println(new String(sbUTF8.array(), StandardCharsets.UTF_8));

        System.out.println("-------------------------------");
        //实现类为 sun.nio.cs.ext.DoubleByte$Encoder
        CharsetEncoder ceGBK = Charset.forName("GBK").newEncoder();
        ByteBuffer sbGBK = ceGBK.encode(CharBuffer.wrap(s1.toCharArray()));
        System.out.println(sbGBK.array().length);
        System.out.println(bytesToHex(sbGBK.array()));
    }

    private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);

    public static String bytesToHex(byte[] bytes) {
        byte[] hexChars = new byte[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars, StandardCharsets.UTF_8);
    }

    public static String bytesToHexFormat(byte[] bytes) {
        StringBuilder builder = new StringBuilder();
        for (byte b: bytes) {
            // %02x中的%x是把数字输出为16进制的格式,%02x是保证输出至少占两个字符的位置
            builder.append(String.format("%02x", b).toUpperCase());
        }
        return builder.toString();
    }

    public static String charsToHex(char[] chars) {
        StringBuilder sb = new StringBuilder();
        for (char each : chars) {
            sb.append(Integer.toHexString((int) each));
        }
        return sb.toString().toUpperCase();
    }

    private static String getBytesCode(byte[] bytes) {
        StringBuilder code = new StringBuilder();
        for (byte b : bytes) {
            code.append(Integer.toHexString(b & 0xff));
        }
        return code.toString();
    }

    public static byte[] int2bytes(int num) {
        byte[] result = new byte[4];
        result[0] = (byte) ((num >>> 24) & 0xff);
        result[1] = (byte) ((num >>> 16) & 0xff);
        result[2] = (byte) ((num >>> 8) & 0xff);
        result[3] = (byte) ((num >>> 0) & 0xff);
        return result;
    }

    /**
     * unicode编码
     */
    public static String stringToUnicode(String str) {
        char[] utfBytes = str.toCharArray();
        StringBuilder unicodeBytes = new StringBuilder();
        for (char utfByte : utfBytes) {
            String hexB = Integer.toHexString(utfByte);
            if (hexB.length() <= 2) {
                hexB = "00" + hexB;
            }
            unicodeBytes.append("\u").append(hexB);
        }
        return unicodeBytes.toString();
    }

    /**
     * unicode解码方式1
     */
    public static String unicodeToString1(String str) {
        int start = 0;
        int end = 0;
        StringBuilder buffer = new StringBuilder();
        while (start > -1) {
            end = str.indexOf("\u", start + 2);
            String charStr = "";
            if (end == -1) {
                charStr = str.substring(start + 2, str.length());
            } else {
                charStr = str.substring(start + 2, end);
            }
            // 16进制parse整形字符串。
            char letter = (char) Integer.parseInt(charStr, 16);
            buffer.append(Character.toString(letter));
            start = end;
        }
        return buffer.toString();
    }

    /**
     * Unicode解码方式2
     */
    private static String unicodeToString2(String str) {
        Pattern pattern = Pattern.compile("(\\u(\w{4}))");
        Matcher matcher = pattern.matcher(str);
        char ch;
        while (matcher.find()) {
            // 本行为核心代码,处理当前的unicode后4位变为16进制,在转换为对应的char中文字符
            ch = (char) Integer.parseInt(matcher.group(2), 16);
            str = str.replace(matcher.group(1), ch + "");
        }
        return str;
    }
}

延伸

MySQL-utf8mb4[编辑]

MySQL字符编码集中有两套UTF-8编码实现:“utf8”和“utf8mb4”,其中“utf8”是一个字最多占据3字节空间的编码实现;而“utf8mb4”则是一个字最多占据4字节空间的编码实现,也就是UTF-8的完整实现。这是由于MySQL在4.1版本开始支持UTF-8编码(当时参考UTF-8草案版本为RFC 2279)时,为2003年,并且在同年9月限制了其实现的UTF-8编码的空间占用最多为3字节,而UTF-8正式形成标准化文档(RFC 3629)是其之后。限制UTF-8编码实现的编码空间占用一般被认为是考虑到数据库文件设计的兼容性和读取最优化,但实际上并没有达到目的,而且在UTF-8编码开始出现需要存入非基本多文种平面的Unicode字符(例如emoji字符)时导致无法存入(由于3字节的实现只能存入基本多文种平面内的字符)。直到2010年在5.5版本推出“utf8mb4”来代替、“utf8”重命名为“utf8mb3”并调整“utf8”为“utf8mb3”的别名,并不建议使用旧“utf8”编码,以此修正遗留问题。[15][16][17][18]

MySQL,utf_unicode_ci和utf8_general_ci

在数据库系统MySQL中有多种字符集,其中utf8_unicode_ci和utf8_general_ci是最常用的,但是utf8_general_ci对某些语言的支持有一些小问题,如果可以接受,那最好使用utf8_general_ci,因为它速度快。否则,请使用较为精确的utf8_unicode_ci,不过速度会慢一些。

多语言无差错,首选“utf_unicode_ci”。

Java-System.out.println

System.out.println(“测试”)。 经过正确的解码后”测试”是unicode保存在内存中的,但是在向标准输出(控制台)输出时,jvm又做了一次转码,它会采用操作系统默认编码(中文操作系统是GBK),将内存中的unicode编码转换为GBK编码,然后输出到控制台。 因为操作系统是中文系统,所以往终端显示设备上打印字符时使用的也是GBK编码。因为终端的编码无法手动改变,所以只要编译时能正确转码,最终的输出不会出现乱码。

全角 & 半角

信息交换用汉字编码字符集·基本集对应的是GB2312,其中也指定了拉丁字母(英文字母)对应的 2 byte编码,这个只有在全角情况下,才输入GB2312字符集中的拉丁字母,而半角时,输入的为ASCII下的拉丁字母编码;

备注:上述查询文档中,字符对应的二进制表示时,借助工具UltraEdit中的十六进制模式

关于全角``半角,几点:

  • 输入汉字,全角、半角,没有区别,对应的GB2312编码完全一致,2 byte;

  • 输入英文字母、符号、数字,有区别:

    • 全角:使用GB2312中对应的编码,占用 2 byte;
    • 半角:使用ASCII中编码,占用 1 byte;
    • 补充:全角存在的意义是,方便显示的整齐和美观;