详解java中的unicode编码(码点)

致谢:

本文参考网页:
Unicode字符集以及UTF-8,UTF-16编码的总结 - vcj1009784814的博客 - CSDN博客

Unicode

unicode的码点从U+0000到U+10FFFF,即共有2^20+2^16-1=1114111个码点。
通常来说,常见字符可以用2个字节(16位)来表示。但还有很多其他表意字符、辅助字符需要表示。

Unicode将所有码点分成了17个代码级别,又称平面

unicode的17个平面

其中第一个平面称为基本的多语言级别(basic multilingual plane, BMP),其他的统称为辅助平面

unicode的平面划分

utf-8

由于unicode会有1个字节-4个字节(最多32位)不等,如果定长存储每个字符都需要占用四个字节,十分浪费空间,utf-8是一种变长存储方式,有1-6个字节不等。具体编码方式如下:

  • 第一个字节提示了这个unicode编码由几个字节组成
    1. 首字节以0开头,表示单字节编码
    2. 首字节以110开头,表示双字节编码,后续字节以10开头
    3. 首字节以1110开头,表示三字节编码,后续字节以10开头
    4. 首字节以11110开头,表示四字节编码,后续字节以10开头
    5. ...
  • 有效位对应的字节数

有效位对应的字节数

utf-16

UTF-16源于UCS-2,UCS-2将字符码点直接映射为字符编码,中间无特别的编码算法。

UCS-2编码方式固定2字节编码,只覆盖了BMP的码点,对于SMP的码点,2字节的16位二进制数是不足以表示的。

而UTF-16扩展了原来的UCS-2编码,解决了辅助平面码点的字符无法表示的问题:

  • BMP中的有效码点,用固定2字节16位来为其编码,数值等于对应的码点,同UCS-2
  • 辅助平面中的有效码点,使用代理对进行编码。在BMP中,有一个范围的码点是未定义的,被称为代理区,其码点范围是0xD800~0xDFFF,共211个码点,代理区又被分为高代理码点低代理码点,其中高代理码点范围是0xD800~0XDBFF,低代理码点范围是0xDC00~0XDFFF,高代理码点和低代理码点结合在一起,就表示一个辅助平面中的字符。由于辅助平面中的字符共有220个(0x10000~0x10FFFF),高代理码点和低代理码点皆有210个取值,两者结合,恰好有220种不同的组合。

详解java中的unicode编码(码点)

也就是说,UTF-16可以表示完unicode中的字符,BMP中的字符需要两个字节,其他的需要四个字节。

java中的unicode

这里使用到一个工具:
Unicode编码转换,UTF编码转换(UTF-8、UTF-16、UTF-32)

char

java中的char是定长以16位(两个字节存储的),并且内部编码为utf-16。
也就是说,一个char只能表示BMP中的字符,若要表示一个辅助平面内的字符,需要两个char。

来看一个BMP中的字符:”我”

Unicode编码:U+6211
UTF8编码:E68891
UTF16BE编码:FEFF6211
UTF16LE编码:FFFE1162

UTF16BE、UTF16LE分别指的是机器中的大端表示和小端表示,前面的FEFFFFFE编译器会帮我们过滤掉,我们这里只看大端表示。

对于UTF-8表示,先将6211转换为二进制编码110 001000 010001,是15个有效位。对照utf-8编码表,可知需要用三个字节(1110xxxx 10xxxxxxxx 10xxxxxxxx)来表示,将有效位填入,得utf-8编码:11100110 10001000 10010001,转换为16进制,就是E68891

对于UTF-16表示,6211是可以用两个字节表示完的,所以UTF-16的编码就是6621

验证:

String

对于辅助平面内的字符,一个char可不够用了,需要由两个char来存储,或是用String来表示。

来看一个emoji字符:”?”

Unicode编码:U+1F449
UTF8编码:F09F9189
UTF16BE编码:FEFFD83DDC49
UTF16LE编码:FFFE3DD849DC

对于UTF-8表示,先将1F449转换为二进制编码11111 010001 001001,是17个有效位。对照utf-8编码表,可知需要用四个字节(11110xxx 10xxxxxxxx 10xxxxxxxx 10xxxxxx)来表示,将有效位填入,得utf-8编码:11110000 10011111 10010001 10001001,转换为16进制,就是F09F9189

对于UTF-16表示,需要用到代理对进行编码:首先用1F449-10000得到F449,将F449转换为20位二进制0000111101 0001001001,高10位转成十六进制得3D,加上D800D83D,后10位转成十六进制得49,加上DC00DC49,所以它的utf-16编码为D83D DC49

验证:

问题:
String.length()会将一个非bmp中的字符算为1还是算为2呢?
测试↓

可见,如果String中存在非bmp中的字符,String会将它算成两个字符长度。看String.length()的源码也可知道这点:

public int length() {
    return value.length;
}

value是String内部的一个字符数组。

java对码点计算的支持

其实String中也提供了计算码点的方法:String.codePointCount(int beginIndex, int endIndex)

利用的是Character类能判断一个码点是否为代理码点:
Character.isHighSurrogate:是否为高位代理码点

public static boolean isHighSurrogate(char ch) {
    // Help VM constant-fold; MAX_HIGH_SURROGATE + 1 == MIN_LOW_SURROGATE
    return ch >= MIN_HIGH_SURROGATE && ch < (MAX_HIGH_SURROGATE + 1);
}

其中MIN_HIGH_SURROGATE=\uD800MAX_HIGH_SURROGATE=\uDBFF,注明了高位代理码点的值边界。

Character.isLowSurrogate:是否为低位代理码点

public static boolean isLowSurrogate(char ch) {
    return ch >= MIN_LOW_SURROGATE && ch < (MAX_LOW_SURROGATE + 1);
}

其中MIN_HIGH_SURROGATE=\uDC00MAX_HIGH_SURROGATE=\uDFFF,注明了高位代理码点的值边界。

要判断字符串内有没有代理码点,只需确定连续的两个码点一个是高位代理码点一个是低位代理码点就行了。

String.codePointCount依赖的方法Character.codePointCountImpl源码:

static int codePointCountImpl(char[] a, int offset, int count) {
    int endIndex = offset + count;
    int n = count;
    for (int i = offset; i < endIndex; ) {
        if (isHighSurrogate(a[i++]) && i < endIndex &&
            isLowSurrogate(a[i])) {
            n--;
            i++;
        }
    }
    return n;
}

如果要遍历一个字符串中的字符,可能需要考虑是否有非kmp中的字符的情况。那么就需要用码点为单位来处理:

反例:

正确处理方式:

String.codePoints()得到一个int类型的流,代表这个码点的unicode编码,用System.out.printf()中的%c格式化输出它,就可以看到这个字符。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/%e8%af%a6%e8%a7%a3java%e4%b8%ad%e7%9a%84unicode%e7%bc%96%e7%a0%81%ef%bc%88%e7%a0%81%e7%82%b9%ef%bc%89/