zoukankan      html  css  js  c++  java
  • 由String的构造方法引申出来的java字符编码

    在String类的constructors中,有一个constructor是将int数组类型转化为字符串:

    1 int[] num = {48,49,50,51,52};
    2 String numStr = new String(num,0,4);
    3 System.out.println(numStr);
    View Code

    输出结果是:

    0123

    这个constructor的作用是将int数组中每一位上的数字转化为在Unicode编码中对应的字符。现在来看看它是怎么转化的。

    源代码:

     1     public String(int[] codePoints, int offset, int count) {
     2         if (offset < 0) {
     3             throw new StringIndexOutOfBoundsException(offset);
     4         }
     5         if (count < 0) {
     6             throw new StringIndexOutOfBoundsException(count);
     7         }
     8         // Note: offset or count might be near -1>>>1.
     9         if (offset > codePoints.length - count) {
    10             throw new StringIndexOutOfBoundsException(offset + count);
    11         }
    12 
    13         final int end = offset + count;
    14 
    15         // Pass 1: Compute precise size of char[]
    16         int n = count;
    17         for (int i = offset; i < end; i++) {
    18             int c = codePoints[i];
    19             if (Character.isBmpCodePoint(c))
    20                 continue;
    21             else if (Character.isValidCodePoint(c))
    22                 n++;
    23             else throw new IllegalArgumentException(Integer.toString(c));
    24         }
    25 
    26         // Pass 2: Allocate and fill in char[]
    27         final char[] v = new char[n];
    28 
    29         for (int i = offset, j = 0; i < end; i++, j++) {
    30             int c = codePoints[i];
    31             if (Character.isBmpCodePoint(c))
    32                 v[j] = (char) c;
    33             else
    34                 Character.toSurrogates(c, v, j++);
    35         }
    36 
    37         this.value  = v;
    38         this.count  = n;
    39         this.offset = 0;
    40     }
    View Code

    代码很简单,但是用到了Character类的三个方法:

    1 Character.isBmpCodePoint(c)
    2 Character.isValidCodePoint(c)
    3 Character.toSurrogates(c, v, j++)

    先来看看第一个方法isBmpCodePoint():

    1     public static boolean isBmpCodePoint(int codePoint) {
    2         return codePoint >>> 16 == 0;
    3         // Optimized form of:
    4         //     codePoint >= MIN_VALUE && codePoint <= MAX_VALUE
    5         // We consistently use logical shift (>>>) to facilitate
    6         // additional runtime optimizations.
    7     }
    View Code

    >>>是移位运算符,codePoint >>> 16 的意思是将codePoint变量无符号右移16位,然后判断是否等于0,这个是在判断什么呢?根据字面意思理解is bmp code point,是否是bmp代码点,也是不明白,然后就去search了一下,于是就引申出了两个概念----代码点与代码单元。

    说到代码点与代码单元,就得先说说Unicode编码的基本概念了。

    1、Unicode的基本概念

    1)编码字符集

      编码字符集是一个字符集,它为每一个字符分配一个唯一数字。Unicode 标准的核心是一个编码字符集,字母“A”的编码为0041和字符“€”的编码为20AC。Unicode标准始终使用十六进制数字,而且在书写时在前面加上前缀“U+”,所以“A”的编码书写为“U+0041”。说白了,就是在编码字符集中,每一个字符都有一个自己的一个唯一的ID。

    2)代码点与代码单元
    Unicode 代码点 U+0041 U+00DF U+6771 U+10400
    表示字形
    UTF-32 代码单元
    00000041
    000000DF
    00006771
    00010400
    UTF-16 代码单元
    0041
    00DF
    6771
    D801 DC00
    UTF-8 代码单元
    41
    C3 9F
    E6 9D B1
    F0 90 90 80


      网摘:“代码点(Code Point)就是指Unicode中为字符分配的编号,一个字符只占一个代码点,例如我们说到字符“汉”,它的代码点是U+6C49.代码单元(Code Unit)则是针对编码方法而言,它指的是编码方法中对一个字符编码以后所占的最小存储单元。例如UTF-8中,代码单元是一个字节,因为一个字符可以被编码为1个,2个或者3个4个字节;在UTF-16中,代码单元变成了两个字节(就是一个char),因为一个字符可以被编码为1个或2个char(你找不到比一个char还小的UTF-16编码的字符,嘿嘿)。说得再罗嗦一点,一个字符,仅仅对应一个代码点,但却可能有多个代码单元(即可能被编码为2个char)。

      说白了,代码点:就是字符所对应的那个“ID”。代码单元:指的是在各种不同的编码方式中(UTF-8,UTF-16),对一个字符编码以后所占的最小存储单元。

    3)增补字符

      16 位编码的所有 65536 个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是,Unicode 标准已扩展到包含多达 1112064 个字符。那些超出原来的16 位限制的字符被称作增补字符

      Java的char类型是固定16bits(两个字节)的。代码点在U+0000 — U+FFFF之内到是可以用一个char完整的表示出一个字符。但代码点在U+FFFF之外的,一个char无论如何无法表示一个完整字符。这样用char类型来获取字符串中的那些代码点在U+FFFF之外的字符就会出现问题。

      于是,有了增补字符。增补字符是代码点在 U+10000 至 U+10FFFF 范围之间的字符,也就是那些使用原始的 Unicode 的 16 位设计无法表示的字符。从 U+0000 至 U+FFFF 之间的字符集有时候被称为基本多语言面 (BMP UBasic Multilingual Plane )。Unicode的代码点可以分成17个代码级别。第一个代码级别称为基本的多语言级别,代码点从U+0000到U+FFFF,其中包括了经典的Unicode代码,其余的16个附加级别,代码点从U+10000到U+10FFFF,其中包括了一些增补字符。因此,每一个 Unicode 字符要么属于 BMP,要么属于增补字符

    2、基于Unicode的具体编码格式

     网摘:

      UTF-32 即将每一个 Unicode 代码点表示为相同值的32位整数。很明显,它是内部处理最方便的表达方式,但是,如果作为一般字符串表达方式,则要消耗更多的内存。

      UTF-16 使用一个或两个未分配的16位代码单元的序列对 Unicode 代码点进行编码。假设U是一个代码点,也就是Unicode编码表中一个字符所对应的Unicode值:
        (1) 如果在BMP级别中,那么16bits(一个代码单元)就足够表示出字符的Unicode值。
        (2)如果U+10FFFF>U>=U+10000,也就是处于增补字符级别中。UTF-16用2个16位来表示出了,并且正好将每个16位都控制在替代区域U+D800-U+DFFF(其中uD800-uDBFF为高代理项 范围,uDC00- uDFFF为低代理项 范围) 中。

      也就是说,在UTF-16中,增补字符的表示方式是由两个代码单元来表示的,原因就是一个代码单元放不下它。那么在java中是如何处理这些增补字符的呢?

      java的处理方式是这样的:对于增补字符U(U+10FFFF>U>=U+10000)。首先,分别初始化2个16位无符号的整数 —— W1和W2。其中W1=110110xxxxxxxxxx(0xD800-0xDBFF),W2 = 110111xxxxxxxxxx(0xDC00-OxDFFF)。然后,将Unicode的高10位分配给W1的低10位,将Unicode 的低10位分配给W2的低10位。这样就可以将20bits的代码点U拆成两个16bits的代码单元。而且这两个代码点正好落在替代区域U+D800-U+DFFF中。 

      UTF-16表示的增补字符怎样才能被正确的识别为增补字符,而不是两个普通的字符呢?答案是通过看它的第一个char是不是在高代理范围内,第二个char是不是在低代理范围内来决定,这也意味着,高代理和低代理所占的共2048个码位(从0xD800到0xDFFF)是不能分配给其他字符的。

    Unicode的编号中,U+D800到U+DFFF是否有字符分配?答案是也没有!这么做的目的是希望基本多语言面中的字符和一个char型的UTF-16编码的字符能够一一对应。(这里就不写代码验证了)

      java具体的是怎么来拆分增补字符的呢?看一个例子:通过两个代码点U+11001,U+1D56B(使用4个字节表示的代码点)以U+1D56B来说

      0x1D56B= 0001 1101 01-01 0110 1011

      将0x1D56B的高10位0001 1101 01分配给W1的低10位组合成110110 0001 1101 01=0xD875
      将0x1D56B的低10位01 0110 1011分配给W2的低10位组合成110111 01 0110 1011=0xDD6B
      这样代码点U+1D56B采用UTF-16编码方式,用2个连续的代码单元U+D875和U+DD6B表示出了

     1 int[] codePoints = {0x11001,0x1d56b}; //增补字符  
     2         String s = new String(codePoints,0,2);  
     3           
     4         System.out.println("s: " + s); 
     5         System.out.println("s.length: " + s.length()); //4,说明length()是按代码单元计算的  
     6         System.out.println("s.charAt(0): " + Integer.toHexString((int)s.charAt(0)));//输出结果表明增补字符并非简单地把两个代码单元拆开  
     7         System.out.println("s.charAt(1): " + Integer.toHexString((int)s.charAt(1)));
     8         System.out.println("s.charAt(2): " + Integer.toHexString((int)s.charAt(2)));
     9         System.out.println("s.charAt(3): " + Integer.toHexString((int)s.charAt(3)));
    10         System.out.println("s.codePointAt(0):" + Integer.toHexString(s.codePointAt(0)));
    View Code

    输出结果是:

    1 s: ??
    2 s.length: 4
    3 s.charAt(0): d804
    4 s.charAt(1): dc01
    5 s.charAt(2): d835
    6 s.charAt(3): dd6b
    7 s.codePointAt(0):11001
    View Code

    可以看到

    字符串的长度为4,说明length()是按代码单元计算的,然后我们看看U+1D56B的拆分结果:

    s.charAt(2): d835
    s.charAt(3): dd6b

    与我们计算的 U+D875 U+DD6B 有出入,该代理代码点不一样,带着这个疑问,我们去看看java代码是如何转化的:

     1 [java.lang.String]
     2 
     3         for (int i = offset, j = 0; i < end; i++, j++) {
     4             int c = codePoints[i];
     5             if (Character.isBmpCodePoint(c))    //判断是不是BMP级别
     6                 v[j] = (char) c;
     7             else
     8                 Character.toSurrogates(c, v, j++);//给出高低代理项
     9         }
    10 
    11 [java.lang.Character]
    12 
    13     static void toSurrogates(int codePoint, char[] dst, int index) {
    14         // We write elements "backwards" to guarantee all-or-nothing
    15         dst[index+1] = lowSurrogate(codePoint);//给出低代理项
    16         dst[index] = highSurrogate(codePoint);//给出高代理项
    17     }
    18 
    19 [java.lang.Character]
    20 
    21     public static final char MIN_LOW_SURROGATE  = 'uDC00';//低代理项最小值
    22 
    23     public static char lowSurrogate(int codePoint) {
    24         return (char) ((codePoint & 0x3ff) + MIN_LOW_SURROGATE);
    25     }
    26 
    27 [java.lang.Character]
    28 
    29     public static final char MIN_HIGH_SURROGATE = 'uD800';//高代理项最小值
    30     public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000;//增补字符最小代码点
    31 
    32     public static char highSurrogate(int codePoint) {
    33         return (char) ((codePoint >>> 10)
    34             + (MIN_HIGH_SURROGATE - (MIN_SUPPLEMENTARY_CODE_POINT >>> 10)));
    35     }
    View Code

    可以看出低代理项与计算方式一样,所以计算出的结果一致,但是高代理项在移位10bit,加上最小高代理项后,又减去了增值字符最小代码点的移位10bit后的值,其实这就相当于,对于增补字符U+1D56B,其操作是对 U+0D56B进行的操作。这里还没有确定为什么会减去这个位,有待考证!

    UTF-8:

    网摘:

      使用一至四个字节的序列对编码 Unicode 代码点进行编码。U+0000 至 U+007F 使用一个字节编码,U+0080 至 U+07FF 使用两个字节,U+0800 至 U+FFFF 使用三个字节,而 U+10000 至 U+10FFFF 使用四个字节。UTF-8 设计原理为:字节值 0x00 至 0x7F 始终表示代码点 U+0000 至 U+007F(Basic Latin 字符子集,它对应 ASCII 字符集)。这些字节值永远不会表示其他代码点,这一特性使 UTF-8 可以很方便地在软件中将特殊的含义赋予某些 ASCII 字符。

    以下是Unicode和UTF-8之间的转换关系表:

    U-00000000 - U-0000007F: 0xxxxxxx          
    U-00000080 - U-000007FF: 110xxxxx 10xxxxxx        
    U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx      
    U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx    
    U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx  
    U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

    可以看到:

    (1)如果一个字节以10开头,一定不是首字节,需要向前查找。

    (2)在一个首字节中,如果以0开头,表示是一个ASCII字符,而开头的连续的1的个数也表示了这个字符的字节数。如1110xxxx表示这个字符由三个字节组成。

    下面来看一个使用各种编码对字符进行编码的例子,如下:

    分析到这里,结合源码,可以看出: Java 以 UTF-16 作为内存的字符存储格式。

    参考:

    http://blog.csdn.net/u010411264/article/details/45258629
    http://blog.csdn.net/cumtwyc/article/details/45080679
    http://blog.csdn.net/mazhimazh/article/details/17708001

  • 相关阅读:
    Scrapy
    关于函数名称和库函数名称冲突的故事
    Hibernate的一级缓存
    spring-mvc 与 openid4java
    openid4java 使用记录[转载]
    Linux定时任务Crontab详解_定时备份
    Spring的线程池ThreadPoolTaskExecutor使用案例
    BZOJ4275 : [ONTAK2015]Badania naukowe
    BZOJ4137 : [FJOI2015]火星商店问题
    BZOJ2832 : 宅男小C
  • 原文地址:https://www.cnblogs.com/gdayq/p/5809435.html
Copyright © 2011-2022 走看看