最近《Java核心技术 卷Ⅰ》,遇到了这个生僻的知识点。想要彻底理解这个知识点需要了解不少东西,整理如下!
一、故事
我们知道,在计算机内部,所有信息最终都是用二进制值表示的。由二进制的数学特性可知,每一个二进制位(bit)有0和1两种状态,一个字节(byte)就可以组合出256种状态,每一个状态可以表示一个字符。为了表示英语字符与二进制位之间的关系,在上个世纪60年代,美国制定了ASCII 码。ASCII 码一共规定了128个字符的编码,比如大写的字母A是65(二进制01000001)。
英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号,从128到255的范围表示的字符集被称"扩展字符集"。但是,等到其他国家(如中国、日本、韩国)使用计算机时,已经没有可以利用的字节状态来表示相关的字符。为此,各个国家为了适应本国语言的需要,纷纷设计本国自己的文字编码集对ASCII 码字符集进行扩充,如简体中文的 GB 系列编码、日文的 SHIFT-JIS 编码等。这些不同的字符集互不兼容,相同的编码可能代表不同的字符,同一个字符在不同的字符集中的编码也往往不同。当信息从一台计算机移植到另一台计算机时,接收方若以错误的编码方式解读,就会出现乱码。不同编码文件之间的交流成为一个亟待解决的问题。
令人欣慰的是,“统一码联盟”和ISO两个国际组织为了解决这一问题,双方开始协同工作。他们采用的方法很简单:废了所有的地区性编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码,这种编码被命名为"Universal Character Set",简称“UCS”,俗称 “Unicode”。
Unicode开始制订时,计算机的存储器容量已经有了极大地发展,空间已不再是重点考虑的问题。由于,设计者天真地以为 16 个比特就可以表示地球上所有仍具活力的文字,于是就直接规定用两个字节来统一表示所有的字符。其实,16个比特仅仅可以表示65536个字符。UCS-2就是Unicode字符集的一种定长的2字节编码(现在已废弃)。
需要注意的是,Unicode只是一个字符集,它只规定了怎么用多个字节的二进制值表示各种文字符号。这些编码值如何在网络中传输是UTF(UCS Transformation Format)规范规定的。当文本只包含英文和数字,如果用Unicode编码就显得特别浪费存储空间(用 ASCII 编码更合适),同样,在用 Unicode编码在网络传输时也比ASCII 编码多一倍的传输。当传输文件比较小的时候,内存资源和网络带宽尚能承受,当文件传输达到上TB的时候,如果 “硬”传,则需要消耗的资源就不可小觑了。为了节约空间,UTF为Unicode字符集提供了多种实现方式。比如常用的UTF-8编码就是Unicode的一种实现方式,顾名思义,UTF-8就是每次8个位传输数据,它是可变长编码。当你的文本是 ASCII 编码的字符时,UTF-8编码用 1 个字节存放;而当文本中包含其它Unicode 字符时,它将按一定算法转换,每个字符使用 1~3 个字节存放。这样便实现了有效节省空间的目的。这也是UTF-8编码被广泛采用的原因。
不过正是因为utf-8编码可变长,一会儿一个字符是占用一个字节,一会儿一个字符占用两个字节,还有的占用三个字节,导致在内存中的格式不统一,运算效率低。而unicode编码虽然占用更多的内存空间,但是在编程过程中或者在内存处理的时候会比utf-8编码更为简单,因为它始终保持一样的长度,处理就会变得更加简单。所以,通常将utf-8编码作为“外码”,用在网络传输和文件保存时;而将unicode编码作为内码,用在文件内容读取到内存中时。
Java语言设计之初就认识到统一字符集(Unicode)的重要性,并积极拥抱了问世不久的Unicode标准。比如,java基本数据类型中char最初描述的就是UCS-2编码中的代码单元。与其他使用8比特字符的语言相比,这是java的主要优势。
但尴尬的是,随着更多字符的引入,尤其是汉语、韩语和日文中的表意文字的引入,使得Unicode远远超出16比特编码的范围。现在,Unicode需要21个比特,表示范围从0x0~0x10FFFF。当单元固定长度16位的UCS-2到达容量上限不能支持更多的 Unicode 字符的时候,Unicode协会放弃了UCS-2。取而代之的是16位的变长编码UTF-16。
在Unicode从16比特向21比特过渡时期,Java语言深受其苦。其中,Java 5.0 版本既要支持Unicode 4.0同时又要保证向后兼容性,所以java开始使用UTF-16作为其内部编码方式。引入码点和编码单元两个概念。
码点(code point)是一个编码表中某个字符对应的二进制代码值,即一个有效的Unicode字符的二进制代码值被称作一个码点。码点一般用十六进制书写,并加上前缀U+,如此一来,21个比特的Unicode,码点范围从U+0000至U+10FFFF。其中码点在U+0000 ~ U+FFFF范围内的字符称为“经典”Unicode字符,超出U+FFFF范围的字符被称作“代理字符”。经典Unicode字符都可以用16个比特表示,通常被称作编码单元(code unit)。代理字符的编码需要两个连续的编码单元。
二、码点和代码单元操作演示
java字符串是由char值序列组成。char数据类型是一个采用UTF-16编码表示的Unicode码点的代码单元,即char表示一个代码单元。
知识点1:String类中length方法将返回给定字符串采用UTF-16编码表示时所需要的代码单元的数量。如下:
String greeting = "Hello";
int n = greeting.length(); //n is 5
String str1 = "hi𝕆";
int m = str1.length(); // m is 4
知识点2:得到实际的字符长度,即码点的数量,如下:
int cpCount_1 = greeting.codePointCount(0, greeting.length()); // cpCount_1 is 5
int cpCount_2 = str1.codePointCount(0, str1.length()); // cpCount_2 is 3
知识点3:调用String类中charAt(n)方法,将返回位置n的代码代码单元,n介于 0~greeting.length() -1之间;
char first = greeting.charAt(0); // first is 'H'
char last = greeting.charAt(1); // last is 'o'
注意,代理(辅助)字符需要2个代码单元才可以表示,所以通过charAt获得的可能不是某个字符,而是辅助字符的一个代码单元。为了避免这个问题,不要使用char类型,它太底层了。
知识点4:获取字符串的第i个码点(),如下:
String greeting = "Hello";
int index = greeting.offsetByCodePoints(0, i);
int cp = greeting.codePointAt(index);
知识点5:Character类中的isSupplementaryCodePoint(int codePoint)方法是一个boolean型方法,用来确定指定字符(Unicode 代码点)codePoint是否在代理字符范围内;
知识点6:Character类中的isSurrogate(char)方法是一个boolean型方法,char对应代码单元是用于表示辅助字符的话就返回true;
三、char和String两种类型互相转换
下面这段程序,是对上述知识点的总结。
public class CharCodePointDemo {
private static String str1 = "hi𝕆";
public static void main(String[] args) {
positiveOrderTraversalAllCodePoint(str1);
negativeOrderTraversalAllCodePoint(str1);
intStreamTraversalAllCodePoint(str1);
}
// 正序遍历字符串的所有码点
public static void positiveOrderTraversalAllCodePoint(String sentence) {
System.out.print(sentence + " length is " + sentence.length() + ". ");
for (int index = 0; index < sentence.length(); ) {
int cp = sentence.codePointAt(index);
System.out.print(cp + " ");
if (Character.isSupplementaryCodePoint(cp)) {
index += 2;
} else {
index++;
}
}
System.out.println();
}
// 逆序遍历字符串的所有码点
public static void negativeOrderTraversalAllCodePoint(String sentence) {
System.out.print(sentence + " length is " + sentence.length() + ". ");
int index = sentence.length();
while (--index >= 0) {
if (Character.isSurrogate(sentence.charAt(index))) {
index --;
}
System.out.print(sentence.codePointAt(index) + " ");
}
System.out.println();
}
// int流-遍历字符串的所有码点
public static void intStreamTraversalAllCodePoint(String sentence) {
System.out.print(sentence + " length is " + sentence.length() + ". ");
int[] codePoints = sentence.codePoints().toArray();
for (int ele : codePoints) {
System.out.print(ele + " ");
}
System.out.println();
}
}
上述代码,执行结果如下:
四、参考资料
- 《java核心技术 卷1》
- 浅谈unicode编码和utf-8编码的关系
- UCS 和 UTF-8 编码
- 细说:Unicode, UTF-8, UTF-16, UTF-32, UCS-2, UCS-4
- 为什么 Java 内部使用 UTF-16 表示字符串?
- 字符编码笔记:ASCII,Unicode 和 UTF-8
- Unicode 及编码方式概述
- [JAVA]isSurrogate(char) 与 isSupplementaryCodePoint(int)