澳门新葡萄京官网注册Unicode字符集和编码格式详解

Unicode和UTF-8/UTF-16/UTF-32的关系

Unicode和UTF-8/UTF-16/UTF-32之间就是字符集和编码的关系。字符集的概念实际上包含两个方面,一个是字符的集合,一个是编码方案。字符集定义了它所包含的所有符号,狭义上的字符集并不包含编码方案,它仅仅是定义了属于这个字符集的所有符号。但通常来说,一个字符集并不仅仅定义字符集合,它还为每个符号定义一个二进制编码。当我们提到GB2312或者ASCII的时候,它隐式地指明了编码方案是GB2312或者ASCII,在这些情况下可以认为字符集与编码方案互等。

但是Unicode具有多种编码方案。Unicode字符集规定的标准编码方案是UCS-2(UTF-16),用两个字节表示一个Unicode字符(UTF-16中两个字节的为基本多语言平面字符,4个字节的为辅助平面字符)。而UCS-4(UTF-32)用4个字节表示一个Unicode字符。另外一个常用的Unicode编码方案–UTF-8用1到4个变长字节来表示一个Unicode字符,并可以从一个简单的转换算法从UTF-16直接得到。所以在使用Unicode字符集时有多种编码方案,分别用于合适的场景。

再通俗一点地讲,Unicode字符集就相当于是一本字典,里面记载着所有字符(即图像)以及各自所对应的Unicode码(与具体编码方案无关),UTF-8/UTF-16/UTF-32码就是Unicode码经过相应的公式计算得到的并且实际存储、传输的数据。

字符集的概念实际上包含两个方面,一个是字符的集合,一个是编码方案。通常来说,一个字符集不仅仅定义字符集合,它还为每个符号定义一个二进制编码。例如当我们提到GB2312或者ASCII的时候,它隐式地指明了编码方案是GB2312或者ASCII。

要理解乱码问题,首先需要理解几个概念:字符集、编码、编码规则、乱码

UTF-16

JVM规范中明确说明了java的char类型使用的编码方案是UTF-16,所以先来了解下UTF-16。

Unicode的编码空间从U+0000到U+10FFFF,共有1112064个码位(code
point)可用来映射字符,,码位就是字符的数字形式。这部分编码空间可以划分为17个平面(plane),每个平面包含2^16(65536)个码位。第一个平面称为基本多语言平面(Basic
Multilingual Plane, BMP),或称第零平面(Plane
0)。其他平面称为辅助平面(Supplementary
Planes)。基本多语言平面内,从U+D800到U+DFFF之间的码位区块是永久保留不映射到Unicode字符。UTF-16就利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。

最常用的字符都包含在BMP中,用2个字节表示。辅助平面中的码位,在UTF-16中被编码为一对16比特长的码元,称作代理对(surrogate
pair),具体方法是:

  • 将码位减去0×10000,得到的值的范围为20比特长的0~0xFFFFF。
  • 高位的10比特的值(值的范围为0~0x3FF)被加上0xD800得到第一个码元或称作高位代理(high
    surrogate),值的范围是0xD800~0xDBFF.由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead
    surrogates)。
  • 低位的10比特的值(值的范围也是0~0x3FF)被加上0xDC00得到第二个码元或称作低位代理(low
    surrogate),现在值的范围是0xDC00~0xDFFF.由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail
    surrogates)。

例如U+10437编码:

  • 0×10437减去0×10000,结果为0×00437,二进制为0000 0000 0100 0011 0111。
  • 分区它的上10位值和下10位值(使用二进制):0000000001 and 0000110111。
  • 添加0xD800到上值,以形成高位:0xD800 + 0×0001 = 0xD801。
  • 添加0xDC00到下值,以形成低位:0xDC00 + 0×0037 = 0xDC37。

由于前导代理、后尾代理、BMP中的有效字符的码位,三者互不重叠,搜索时一个字符编码的一部分不可能与另一个字符编码的不同部分相重叠。所以可以通过仅检查一个码元(构成码位的基本单位,2个字节)就可以判定给定字符的下一个字符的起始码元。

但是Unicode字符集例外,它存在着几种不同的编码方式,例如:

  1. 字符集:

java中的codepoint相关

对于一个字符串对象,其内容是通过一个char数组存储的。char类型由2个字节存储,这2个字节实际上存储的就是UTF-16编码下的码元。我们使用charAt和length方法的时候,返回的实际上是一个码元和码元的数量,虽然一般情况下没有问题,但是如果这个字符属于辅助平面字符,以上2个方法便无法得到正确的结果。正确的处理方式如下:

int character = aString.codePointAt(i);
int length = aString.codePointCount(0, aString.length());

需要注意codePointAt的返回值,是int而非char,这个值就是Unicode码。

codePointAt方法调用了codePointAtImpl:

static int codePointAtImpl(char[] a, int index, int limit) {
        char c1 = a[index];
        if (isHighSurrogate(c1) && ++index < limit) {
            char c2 = a[index];
            if (isLowSurrogate(c2)) {
                return toCodePoint(c1, c2);
            }
        }
        return c1;
    }

isHighSurrogate方法判断下标字符的2个字节是否为UTF-16中的前导代理(0xD800~0xDBFF):

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);
    }

public static final char MIN_HIGH_SURROGATE = 'uD800';
public static final char MAX_HIGH_SURROGATE = 'uDBFF';

然后++index,isLowSurrogate方法判断下一个字符的2个字节是否为后尾代理(0xDC00~0xDFFF):

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

public static final char MIN_LOW_SURROGATE  = 'uDC00';
public static final char MAX_LOW_SURROGATE  = 'uDFFF';

toCodePoint方法将这2个码元组装成一个Unicode码:

public static int toCodePoint(char high, char low) {
        // Optimized form of:
        // return ((high - MIN_HIGH_SURROGATE) << 10)
        //         + (low - MIN_LOW_SURROGATE)
        //         + MIN_SUPPLEMENTARY_CODE_POINT;
        return ((high << 10) + low) + (MIN_SUPPLEMENTARY_CODE_POINT
                                       - (MIN_HIGH_SURROGATE << 10)
                                       - MIN_LOW_SURROGATE);
    }

这个过程就是以上将一个辅助平面的Unicode码位转换成2个码元的逆过程。

所以,枚举字符串的正确方法:

for (int i = 0; i < aString.length();) {
    int character = aString.codePointAt(i);
    //如果是辅助平面字符,则i+2
    if (Character.isSupplementaryCodePoint(character)) i += 2;
    else ++i;
}

将codePoint转换为char[]可调用Character.toChars方法,然后可进一步转换为字符串:

new String(Character.toChars(codePoint));

toChars方法所做的就是以上将Unicode码位转换为2个码元的过程。

参考:

  • 维基百科
  • UTF-8
  • UTF-16
  • UTF-32;

字符(Character)是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。字符集(Character
set)是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、
GB18030字符集、Unicode字符集等。其实字符集简单了来说,就是一张表格,是
id 和字符的对应表。

其中UTF-8和UTF-16采用可变长度编码,UTF-32固定采用固定长度编码;

  1. 各种编码:

维基百科对Unicode的描述如下
Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位可以用来映射字符;Unicode的编码空间可划分为17个平面,每个平面包括65,536(即2^16)
个码位。17个平面的码位可表示为U+xx0000到U+xxFFFF,其中xx表示平面,从0x00到0x10。第一个平面称为基本多语言平面,其它平面称为辅助平面.基本多语言平面内的U+D800到U+DFFF之间的码位是永久保留的,不会映射到任何Unicode字符。

一种编码格式必须选定一个字符集。比如 UTF-8和 UTF-16 / UTF-32 选用
Unicode 字符集,GB2312选用GB2312字符集字符集。

UTF-8

UTF-8使用1至4个字节为每个字符编码:

  • 0x00-0x7F:表示US-ASCII字符,共128个码位,占用1个字节;
  • 0x80-0x7FF:第一个字节由110开始,接着单字节由10开始,共1920个码位,占用2个字节;
  • 0x900-0xD7FF,0xE000-0xFFFF:第一个字节由1110开始,接着的字节由10开始;占用3个字节;
  • 0x10000-0x10FFFF:第一个字节由11110开始,接着的字节由10开始,占用4个字节。

对于UTF-8编码中的任意字节B,

  1. 如果B的第一位为0,则B独立的表示一个字符(ASCII码);
  1. 如果B的第一位为1,第二位为0,则B为一个多字节字符中的一个字节(非ASCII字符);
  2. 如果B的前两位为1,第三位为0,则B为两个字节表示的字符中的第一个字节;
  3. 如果B的前三位为1,第四位为0,则B为三个字节表示的字符中的第一个字节;
  4. 如果B的前四位为1,第五位为0,则B为四个字节表示的字符中的第一个字节;
    因此,对UTF-8编码中的任意字节,根据第一位,可判断是否为ASCII字符;根据前二位,可判断该字节是否为一个字符编码的第一个字节;根据前四位(如果前两位均为1),可确定该字节为字符编码的第一个字节,并且可判断对应的字符由几个字节表示;根据前五位(如果前四位为1),可判断编码是否有错误或数据传输过程中是否有错误。

例如,希伯来语字母aleph(א)的Unicode代码是U+05D0,按照以下方法改成UTF-8:

  1. 它属于U+0080到U+07FF区域,说明它使用双字节,110yyyyy 10zzzzzz.
  2. 十六进制的0x05D0换算成二进制就是101-1101-0000.
  3. 这11位数按顺序放入”y”部分和”z”部分:11010111 10010000.
  4. 最后结果就是双字节,用十六进制写起来就是0xD7
    0x90,这就是这个字符aleph(א)的UTF-8编码。
  1. 不同字符集的编码、解码规则:

UTF-16

(1)UTF-8的编码规则:

基本多语言平面

码位范围为U+0000到U+FFFF,包含了最常见的字符,UTF-16将这个范围内的码位编码为2个字节,数值等于对应的Unicode码位即0x0000至0xFFFF

UTF-8是一种变长字节编码方式。对于某一个字符的UTF-8编码,如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的位数,其余各字节均以10开头。UTF-8最多可用到6个字节。 
如表: 
1字节 0xxxxxxx 
2字节 110xxxxx 10xxxxxx 
3字节 1110xxxx 10xxxxxx 10xxxxxx 
4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 
5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
因此UTF-8中可以用来表示字符编码的实际位数最多有31位,即上表中x所表示的位。除去那些控制位(每字节开头的10等),这些x表示的位与UNICODE编码是一一对应的,位高低顺序也相同。 
实际将UNICODE转换为UTF-8编码时应先去除高位0,然后根据所剩编码的位数决定所需最小的UTF-8编码位数。 
因此那些基本ASCII字符集中的字符(UNICODE兼容ASCII)只需要一个字节的UTF-8编码(7个二进制位)便可以表示。

辅助平面

码位范围为U+10000到U+10FFFF,UTF-16将这个范围内的码位编码为4个字节,称为代理对(surrogate
pair)。
具体的编码方式如下:

  1. 码位减去0x10000,得到的值的范围为0x00000至0xFFFFF,长度为20个比特;
  2. 高位的10比特的值(0至0x3FF)加上0xD800得到第一个码元或称作高位代理(high
    surrogate),值的范围是0xD800至0xDBFF;由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead
    surrogates);
  3. 低位的10比特的值(0至0x3FF)加上0xDC00得到第二个码元或称作低位代理(low
    surrogate),值的范围是0xDC00至0xDFFF;由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail
    surrogates)。

综上所述,前导代理、后尾代理和基本语言平面的码位,三者互不重叠,因此可以通过检查一个码元就可以判定给定字符的下一个字符的起始码元,这意味着UTF-16是自同步的。

例如U+10437编码:

  1. 大于0xFFFF,采用4个字节进行编码;
  2. 0x10437减去0x10000,结果为0x00437,二进制为0000 0000 0100 0011 0111。
  3. 分区它的上10位值和下10位值(使用二进制):0000000001 and 0000110111。
  4. 添加0xD800到上值,以形成高位:0xD800 + 0x0001 = 0xD801。
  5. 添加0xDC00到下值,以形成低位:0xDC00 + 0x0037 = 0xDC37。

UTF-16编码存在三种编码格式:

  • UTF-16BE:

    Big Endian,最低位地址存放高位字节

  • UTF-16LE:

    Little Endian,最高位地址存放高位字节

  • UTF-16:

    高字节在前还是低字节在前有流中的前两个字节确定,FEFF表示Big
    Endian,FFFE表示Little Endian;

public class TestUTF {
    public static void main(String[] args) throws Exception {
        String str = "中";

        //------------编码

        //Java里使用的是UTF-16BE方式来存储数据的
        System.out.println(Integer.toHexString(str.charAt(0)).toUpperCase());//4E2D

        /*
         * 进行编码时,因为 UTF-16 编码方式本身未指定字节顺序标记,所以默认使用 Big Endian 字节
         * 顺序编码,并将 Big Endian 字节顺序标记写入到流中,所以流前面多了 FE FF 二字节的高字节
         * 顺序标记
         */
        System.out.println(byteToHex(str.getBytes("utf-16")));//FE FF 4E 2D

        /*
         * 进行编码时,UTF-16BE 和 UTF-16LE charset 不会将字节顺序标记写入到流中
         * 即它们所编出的码每个字符只占二个字节,要注意的是解码时要使用同样的编码
         * 方式,不然会出现问题乱码
         */
        System.out.println(byteToHex(str.getBytes("utf-16BE")));//4E 2D
        System.out.println(byteToHex(str.getBytes("utf-16LE")));//2D 4E

        //使用 utf-16BE 对高字节序进行解码,忽略字节顺序标记,即不会将流前二字节内容看作字节序标记
        System.out.println(new String(new byte[]{0x4E, 0x2D}, "utf-16BE"));// 中
        //使用 utf-16LE 对低字节序进行解码,忽略字节顺序标记,即不会将流前二字节内容看作字节序标记
        System.out.println(new String(new byte[]{0x2D, 0x4E}, "utf-16LE"));// 中

        //------------解码

        /*
         * 使用 utf-16 进行解码时,会根据流前两字节内部来确定是低还是高字节顺序,如果流的前两字节
         * 内部不是 高字节序 FE FF,也不是低字节序 FF FE时,则默认使用 高字节序 方式来解码
         */

        //因为0x4E,0x2D为“中”字的高字节表示,所以前面需要加上 FE FF 字节顺序标记来指示它
        System.out.println(new String(new byte[]{(byte) 0xFE, (byte) 0xFF, 0x4E, 0x2D}, "utf-16"));//中

        //因为0x2D,0x4E为“中”字的低字节表示,所以前面需要加上 FF FE 字节顺序标记来指示它
        System.out.println(new String(new byte[]{(byte) 0xFF, (byte) 0xFE, 0x2D, 0x4E,}, "utf-16"));//中


        //使用默认 高字节顺序 方式来解码,
        System.out.println(new String(new byte[]{0x4E, 0x2D}, "utf-16"));//中

        //因为 0x2D,0x4E 为“中”的低字节序,但 utf-16 默认却是以 高字节序来解的,所以出现乱码
        System.out.println(new String(new byte[]{0x2D, 0x4E,}, "utf-16"));//?
    }

    public static String byteToHex(byte[] bt) {
        StringBuilder sb = new StringBuilder(4);
        for (int b : bt) {
            sb.append(Integer.toHexString(b&0xff).toUpperCase());
            sb.append(" ");
        }
        return sb.toString();
    }
}

Windows平台,在UTF-8文件的开首,很多时候都放置一个U+FEFF字符(UTF-8以EF,BB,BF代表),以表明这个文本文件是以UTF-8编码

(2)UTF-16的编码规则:

UTF-32

采用4个字节进行编码,就空间而已,其效率最差;另外其不像UTF-16,可以很容易的判断出下一个字符的开始位置,因此并不如其它Unicode编码用得广泛;

UTF-16是Unicode字符集的一种转换方式,即把Unicode的码位转换为16比特长的码元串行,以用于数据存储或传递。

Java字符编码

Java虚拟机规范中明确说明了java的char类型使用的编码方案是UTF-16,而我们知道char类型由2个字节存储,这两个字节实际上存储的就是UTF-16编码下的码元;而通过前文可以知道,对于辅助平面字符,需要由4个字节来进行表述;因此我们通过charAt或length方法返回的码元或码元数量只是对于基本语言平面字符正确;正确的处理方式如下:

public int codePointAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return Character.codePointAtImpl(value, index, value.length);
    }

public int codePointCount(int beginIndex, int endIndex) {
        if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) {
            throw new IndexOutOfBoundsException();
        }
        return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex);
    }

可以看到,此时返回结果为int类型,而不是char,因为char由2个字节表示,而辅助平面字符需要4个字节才能表示。因此Java中如果参数是char,则说明不支持辅助平面字符;如果为int,则支持基本平面和辅助平面字符;具体的方法可以参加Character类;

2.2.1 从U+D800到U+DFFF的码位(代理区)

跨平台(语言)调用

如果所述,我们知道Java是采用UTF-16
BigEndian存储字符的,那么如果跨语言调用,比如JNI对字符是如何处理的呢?
JNI中提供了函数GetStringUTFChars函数,将字符从UTF-16转化为UTF-8:

JNI_ENTRY(const char*, jni_GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy))
  JNIWrapper("GetStringUTFChars");
#ifndef USDT2
  DTRACE_PROBE3(hotspot_jni, GetStringUTFChars__entry, env, string, isCopy);
#else /* USDT2 */
 HOTSPOT_JNI_GETSTRINGUTFCHARS_ENTRY(
                                     env, string, (uintptr_t *) isCopy);
#endif /* USDT2 */
  oop java_string = JNIHandles::resolve_non_null(string);
  size_t length = java_lang_String::utf8_length(java_string);
  char* result = AllocateHeap(length + 1, "GetStringUTFChars");
  java_lang_String::as_utf8_string(java_string, result, (int) length + 1);
  if (isCopy != NULL) *isCopy = JNI_TRUE;
#ifndef USDT2
  DTRACE_PROBE1(hotspot_jni, GetStringUTFChars__return, result);
#else /* USDT2 */
 HOTSPOT_JNI_GETSTRINGUTFCHARS_RETURN(
                                      result);
#endif /* USDT2 */
  return result;
JNI_END

可以看到它是通过java_lang_String::as_utf8_string方法进行转换的:

char* java_lang_String::as_utf8_string(oop java_string, char* buf, int buflen) {
  typeArrayOop value  = java_lang_String::value(java_string);
  int          offset = java_lang_String::offset(java_string);
  int          length = java_lang_String::length(java_string);
  jchar* position = (length == 0) ? NULL : value->char_at_addr(offset);
  return UNICODE::as_utf8(position, length, buf, buflen);
}

char* UNICODE::as_utf8(jchar* base, int length, char* buf, int buflen) {
  u_char* p = (u_char*)buf;
  u_char* end = (u_char*)buf + buflen;
  for (int index = 0; index < length; index++) {
    jchar c = base[index];
    if (p + utf8_size(c) >= end) break;      // string is truncated
    p = utf8_write(p, base[index]);
  }
  *p = '';
  return buf;
}

static u_char* utf8_write(u_char* base, jchar ch) {
  if ((ch != 0) && (ch <=0x7f)) {//对于基本语言平面字符,UTF-16编码的数值和UTF-8相同,比如字符"a",UTF-16 BE编码为"0x0061",UTF-8为0x61
    base[0] = (u_char) ch;
    return base + 1;
  }

//对于UTF-16编码,0xFFFF范围内的编码和Unicode编码相同;对于UTF-8,0x80-0x7FF范围内第一个字节由110开始,接着单字节由10开始,共1920个码位,占用2个字节
  if (ch <= 0x7FF) {
    /* 11 bits or less. */
    unsigned char high_five = ch >> 6;
    unsigned char low_six = ch & 0x3F;
    base[0] = high_five | 0xC0; /* 110xxxxx */
    base[1] = low_six | 0x80;   /* 10xxxxxx */
    return base + 2;
  }
//对于UTF-8,0x900-0xD7FF,0xE000-0xFFFF范围内第一个字节由1110开始,接着的字节由10开始;占用3个字节;可以看到,此处并不支持辅助平面字符;
  /* possibly full 16 bits. */
  char high_four = ch >> 12;
  char mid_six = (ch >> 6) & 0x3F;
  char low_six = ch & 0x3f;
  base[0] = high_four | 0xE0; /* 1110xxxx */
  base[1] = mid_six | 0x80;   /* 10xxxxxx */
  base[2] = low_six | 0x80;   /* 10xxxxxx */
  return base + 3;
}

int UNICODE::utf8_size(jchar c) {
  if ((0x0001 <= c) && (c <= 0x007F)) return 1;//US-ASCII, UTF-8占用一个字节
  if (c <= 0x07FF) return 2;//UTF-8编码占用两个字节
  return 3;//UTF-8编码占用三个字节
}

因为Unicode字符集的编码值范围为0-0x10FFFF,而大于等于0x10000的辅助平面区的编码值无法用2个字节来表示,所以Unicode标准规定:基本多语言平面内,U+D800..U+DFFF的值不对应于任何字符,为代理区。因此,UTF-16利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。

参考资料

维基百科UTF-8
维基百科UTF-16
维基百科UTF-32
聊聊java中codepoint和UTF-16相关的一些事

但是在使用UCS-2的时代,U+D800..U+DFFF内的值被占用,用于某些字符的映射。但只要不构成代理对,许多UTF-16编码解码还是能把这些不符合Unicode标准的字符映射正确的辨识、转换成合规的码元.
按照Unicode标准,这种码元串行本来应算作编码错误.

2.2.2 从U+0000至U+D7FF以及从U+E000至U+FFFF的码位

第一个Unicode平面(BMP),码位从U+0000至U+FFFF(除去代理区),包含了最常用的字符。UTF-16与UCS-2编码在这个范围内的码位为单个16比特长的码元,数值等价于对应的码位。BMP中的这些码位是仅有的码位可以在UCS-2被表示。

2.2.3 从U+10000到U+10FFFF的码位

辅助平面(Supplementary
Planes)中的码位,大于等于0x10000,在UTF-16中被编码为一对16比特长的码元(即32bit,4Bytes),称作
code units called a 代理对(surrogate pair),具体方法是:

Ø 码位减去0x10000,
得到的值的范围为20比特长的0..0xFFFFF(因为Unicode的最大码位是0x10ffff,减去0x10000后,得到的最大值是0xfffff,所以肯定可以用20个二进制位表示),写成二进制形式:yyyy
yyyy yyxx xxxx xxxx。

Ø
高位的10比特的值(值的范围为0..0x3FF)被加上0xD800得到第一个码元或称作高位代理(high
surrogate),
值的范围是0xD800..0xDBFF。由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead
surrogates)。

Ø
低位的10比特的值(值的范围也是0..0x3FF)被加上0xDC00得到第二个码元或称作低位代理(low
surrogate), 现在值的范围是0xDC00..0xDFFF。
由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail
surrogates)。

Ø 最终的UTF-16(4字节)的编码(二进制)就是:110110yyyyyyyyyy
110111xxxxxxxxxx。

按照上述规则,Unicode编码0x10000-0x10FFFF的UTF-16编码有两个WORD,第一个WORD的高6位是110110,第二个WORD的高6位是110111。可见,第一个WORD的取值范围(二进制)是11011000
00000000到11011011
11111111,即0xD800-0xDBFF。第二个WORD的取值范围(二进制)是11011100
00000000到11011111
11111111,即0xDC00-0xDFFF。上面所说的从U+D800到U+DFFF的码位(代理区),就是为了将一个WORD(2字节)的UTF-16编码与两个WORD的UTF-16编码区分开来。

由于高位代理、低位代理、BMP中的有效字符的码位,三者互不重叠,搜索是简单的:
一个字符编码的一部分不可能与另一个字符编码的不同部分相重叠。这意味着UTF-16是自同步(self-synchronizing):可以通过仅检查一个码元就可以判定给定字符的下一个字符的起始码元。
UTF-8也有类似优点,但许多早期的编码模式就不是这样,必须从头开始分析文本才能确定不同字符的码元的边界。

由于最常有的字符都在基本多文种平面中,许多软件的处理代理对的部分往往得不到充分的测试。这导致了一些长期的bug与潜在安全漏洞,甚至在广为流行得到良好评价的应用软件。

(3)UTF-32的编码规则:

UTF-32编码以32位无符号整数为单位。Unicode的UTF-32编码就是其对应的
32位无符号整数。
字节序
根据字节序的不同,UTF-16可以被实现为UTF-16LE或UTF-16BE,UTF-
32可以被实现为UTF-32LE或UTF-32BE。例如:
Unicode编码 ║ UTF-16LE ║ UTF-16BE ║ UTF32-LE ║  UTF32-BE 
0x006C49 ║ 49 6C ║ 6C 49 ║ 49 6C 00 00 ║ 00 00 6C 49 
0x020C30 ║ 43 D8 30 DC ║ D8 43 DC 30 ║ 30 0C 02 00 ║ 00 02 0C
30 
那么,怎么判断字节流的字节序呢?Unicode标准建议用BOM(Byte Order
Mark)来区分字节序,即在传输字节流前,先传输被作为BOM的字符”零宽无中断空格”。这个字符的编码是FEFF,而反过来的FFFE(UTF-
16)和FFFE0000(UTF-32)在Unicode中都是未定义的码位,不应该出现在实际传输中。下表是各种UTF编码的BOM:
UTF编码 ║ Byte Order Mark 
UTF-8 ║ EF BB BF 
UTF-16LE ║ FF FE 
UTF-16BE ║ FE FF 
UTF-32LE ║ FF FE 00 00 
UTF-32BE ║ 00 00 FE FF

 

上述三种编码方式各有优劣:

其中
UTF-8是变长的,最节省空间的,UTF-32是空间开销最大的。UTF-16空间开销折中,但是有个缺点就是缺少某些字符的编码。

 

4.乱码:

为什么会出现乱码呢?有两种可能的原因,一种是选择了错误的编码,一种是选择了错误的解码。

但是有的乱码是可逆的,有的是不可逆的。

用 ASCII 进行解码是可逆的。因为ASCII
的256个字符集,都可以用8位二进制数表示。而像
UTF-16这种的,有的二进制表示是没有对应的字符集的,找不到,就是不可逆的。能在字符集找到的就是可逆的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注