Java char 和 String 的区别: 字符编码及其存储 |
您所在的位置:网站首页 › char和string的区别 › Java char 和 String 的区别: 字符编码及其存储 |
一、 ASCII码
上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。一个字节(8bit)一共 可以用来表示256种不同的状态。ASCII码一共规定了128个字符的编码,比如大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印 出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。 二、非ASCII编码 英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。 于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲 国家使用的编码体系,可以表示最多256个符号。 但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语 编码中代表了é,在希伯来语编码中却代表了字母Gimel (ג),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0--127表 示的符号是一样的,不一样的只是128--255的这一段。 至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。一个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达一个符号。 比如,简体中文常见的编码方式是GB2312,使用两个字节表示一个汉字,所以理论上最多可以表示65536个符号。 三、Unicode 要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。可以想象,如果有一种编码,将世界上所有的符号都 纳入其中,每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。
2的16次方(65536)个号码组成一个平面 新的设计将字符集中的所有字符分为 17 个 代码平面(code plane)。 U+0000 ~ U+FFFF 基本多语言平面BMP(Basic Multilingual Plane), U+10000 ~ U+10FFFF 辅助平面SMP (Supplementary Plane), 这些处于辅助平面的字符我们称作 增补字符(supplementary characters)。 四、Unicode的问题 需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。 javascript使用Unicode字符集编写的 utf(Unicode Transformation Format) 4.1 UTF-324字节表示一个字符,完全对应Unicode编码,比如,字母a为0x00000061 缺点:浪费空间,比相同的ASCII编码文件大四倍
4.2 UTF-16 变长编码,长度为2或4字节 编号范围 字节 0x0000 - 0xFFFF 20x010000 - 0x10FFFF 4
于是就有一个问题,当我们遇到两个字节,怎么看出它本身是一个字符,还是需要跟其他两个字节放在一起解读? 在基本平面内,从U+D800到U+DFFF是一个空段,即这些码点不对应任何字符。因此,这个空段可以用来映射辅助平面的字符。 U+D800到U+DBFF(空间大小210),称为高位(H), U+DC00到U+DFFF(空间大小210),称为低位(L)。 这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。
所以,当我们遇到两个字节,发现它的码点在U+D800到U+DBFF之间,就可以断定,紧跟在后面的两个字节的码点, 应该在U+DC00到U+DFFF之间,这四个字节必须放在一起解读。
Unicode码点转成UTF-16的时候,首先区分这是基本平面字符,还是辅助平面字符。如果是前者,直接将码点转为对应的十六进制形式,长度为两字节。 U+597D = 0x597D如果是辅助平面字符,使用转码公式: H = Math.floor((c-0x10000) / 0x400)+0xD800 L = (c - 0x10000) % 0x400 + 0xDC00下面通过将 U+64321 这个处于辅助平面的字符进行 UTF-16 编码的实例来讲解辅助平面字符的编码方式。 1、首先将这个字符的代码点减去 0x10000,得到长度为 20 bit 的一个值,这个值的范围必然在 0x0000 ~ 0xFFFF之内。 V = 0x64321 Vx= V - 0x10000 = 0x54321 = 0101 0100 0011 0010 00012、将 Vx 的高位 10 bit 的值作为高位代理的运算基数 Vh,将低位 10 bit 的值作为低位代理的运算基数 Vl。 这两个 10 bit 的值的取值范围都必然在 0x0000 ~ 0x3FF 之间。 Vh = 0101 0100 00 Vl = 11 0010 00013、将 Vh 和 Vl 分别与高位代理区和低位代理区起始位置的代码点进行 按位或 运算,得到的结果就是这个处于辅助平面的字符 U+64321 的 UTF-16 编码。 W1 = 0xD800 = 1101 1000 0000 0000 W2 = 0xDC00 = 1101 1100 0000 0000 W1 = W1 | Vh = 1101 1000 0000 0000 | 01 0101 0000 = 1101 1001 0101 0000 = 0xD950 W2 = W2 | Vl = 1101 1100 0000 0000 | 11 0010 0001 = 1101 1111 0010 0001 = 0xDF214、所以最终 U+64321 这个字符就被编码成了由高位代理和低位代理组成的一个代理对,我们需要同时用 0xD950 和 0xDF21 来表示这个字符。
那么,为什么JavaScript不选择更高级的UTF-16,而用了已经被淘汰的UCS-2呢? 答案很简单:非不想也,是不能也。因为在JavaScript语言出现的时候,还没有UTF-16编码。 由于JavaScript只能处理UCS-2编码,造成所有字符在这门语言中都是2个字节,如果是4个字节的字符,会当作两个双字节的字符处理。 JavaScript的字符函数都受到这一点的影响,无法返回正确结果。 4.3 UTF-8 人们真正需要的是一种节省空间的编码方法,这导致了UTF-8的诞生。UTF-8是一种变长的编码方法,字符长度从1个字节到4个字节不等。 越是常用的字符,字节越短,最前面的128个字符,只使用1个字节表示,与ASCII码完全相同。 编号范围 字节 0x0000 - 0x007F 10x0080 - 0x07FF 20x0800 - 0xFFFF 30x010000 - 0x10FFFF 4 五、Java char 和 String 的区别由于 Java 采用的是 16 位的 Unicode 字符集,即 UTF-16,所以在 Java 中 char 数据类型是定长的,其长度永远只有 16 位,char 数据类型永远只能表示 代码点在 U+0000 ~ U+FFFF 之间的字符,也就是在 BMP 内的字符。 char c1 = '𝌆'; char c2 = '\u64321';如上编写的代码,使用 char 数据类型来保存辅助平面的字符,编译器将会报错 Invalid character constant。 如果代码点超过了这个范围,即使用了增补字符,那么 char 数据类型将无法支持, 因为增补字符需要 32 位的长度来存储,我们只能转而使用 String 来存储这个字符。
5.1获取字符串长度 一个完整的“字符”是一个code point;一个code point可以对应1到2个code unit;一个code unit是16位。 只有只需1个code unit的code point才可以完整的存在char里。但String作为char的序列,可以包含由两个code unit组成的“surrogate pair”来表示需 要2个code unit表示的UTF-16 code point。为此Java的标准库新加了一套用于访问code point的API,而这套API就表现出了UTF-16的变长特性。 查看 String 的源码,我们可以看到其底层实际是使用一个 char 类型数组在存储我们的字符。 /** The value is used for character storage. */ private final char value[]; /** * Returns the length of this string. * The length is equal to the number of Unicode code units in the string. * * @return the length of the sequence of characters represented by this object. */ public int length() { return value.length; }
字符串长度就是char数组的长度 String tt = "我喜欢𝌆这个字符"; System.out.println(tt.length()); // 9字符串 tt 中应该只有 8 个字符,然而实际输出却是 9 个。上面我们已经讲过 Java 采用的是 16 位的 Unicode 字符集,所以在 Java 中一个代码单元的长度也是 16 位。 一个增补字符需要两个代码单元来表示,所以 tt 字符串中的字符 𝌆 需要占用 value 数组的两个位置,这就是输出 9 而不是 8 的原因。
这里就体现了 Java 中 char 类型无法表示一个增补字符的问题。 其实我们仔细阅读 length() 方法上的注释也可以知道,这个方法返回的是这个字符串中 Unicode 代码单元的数量(The length is equal to the number of Unicode code units in the string)。
那么有没有什么办法能够获取到我们想要的 8 呢?我们可以调用 codePointCount(int beginIndex, int endIndex) 这个方法来实现。 顾名思义,这个方法返回的是字符串中指定部分的代码点的数量,不管你是处于 BMP 范围内的字符还是辅助平面的字符,你的代码点都只能是一个, 所以这就可以精确的得到字符串中的字符数量,我们来看这个方法的实现: 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); }
for 循环里就是核心逻辑,依次判断字符串中的第 n 个字符和 n+1 个字符是否分别落在高位代理区和低位代理区。 如果满足判断条件,则默认返回的字符总数-1。 static int codePointCountImpl(char[] a, int offset, int count) { int endIndex = offset + count; int n = count; for (int i = offset; i |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |