由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

 

posted @ 2016-08-26 16:32  挟天子以令诸侯  阅读(677)  评论(1编辑  收藏  举报