char: The char data type is a single 16-bit Unicode character. It has a minimum value of '\u0000' (or 0) and a maximum value of '\uffff' (or 65,535 inclusive).
从java的文档中这句话我们可以看出,java中的字符内部是以UTF-16编码方式表示的,最小值是 \u0000 (0),最大值是\uffff(65535), 也就是一个字符以2个字节来表示。那这个意思是Java最多只能表示 65535 个字符?当然这肯定是不可能的,现在16位的Char类型已经不能满足描述所有Unicode字符的需要了,java采用了新的方法来解决这个问题
先引出两个概念,代码点,代码单元。
1.代码点(code point):与编码表中的某个字符对应的代码值
在Unicode标准中,代码点采用十六进制书写,并加上前缀U+。例U+0041就是字母A的代码点Unicode的代码点可以分成17个代码级别(code plane)。第一个代码级别成为基本的多语言级别,代码点从U+0000到U+FFFF,其中包括了经典的Unicode代码
其余的16个附加级别,代码点从U+10000到U+10FFFF,其中包括了一些辅助字符(增补字符)
上述提到了增补字符,这个我们后面在进行叙述。
2.代码单元(code unit):在第一代码级别中,每个字符用16位表示(UTF-16的代码单元就是两个字节),辅助字符在UTF-16中就需要采用两个连续的代码单元进行编码
UTF-16编码采用不同长度的编码表示所有的Unicode代码点.
通俗理解 :
代码点 : Unicode中编码的各个字符
代码单元 : 在具体编码形式中的最小单位。比如 UTF-16 中一个 code unit 为 16 bits,UTF-8 中一个 code unit 为 8 bits。一个 code point 可能由一个或多个 code unit(s) 表示。在 U+10000 之前的 code point 可以由一个 UTF-16 code unit 表示,U+10000 及之后的 code point 要由两个 UTF-16 code units 表示
Unicode(代码点) | 常用字符 | 辅助字符 |
数量(代码单元) | 一个代码单元 | 一对代码单元 |
由上图我们可以看出,同一个代码点在不同的编码方式中的代码单元可能不同
引入一个例子:
public class Main {
public static void main(String[] args) {
// 中文常见字
String s = "你好";
System.out.println("1. string length =" + s.length());
System.out.println("1. string bytes length =" + s.getBytes().length);
System.out.println("1. string char length =" + s.toCharArray().length);
System.out.println();
// emojis
s = "??";
System.out.println("2. string length =" + s.length());
System.out.println("2. string bytes length =" + s.getBytes().length);
System.out.println("2. string char length =" + s.toCharArray().length);
System.out.println();
// 中文生僻字
s = "?妹";
System.out.println("3. string length =" + s.length());
System.out.println("3. string bytes length =" + s.getBytes().length);
System.out.println("3. string char length =" + s.toCharArray().length);
System.out.println();
}
}
运行这个程序,你觉得输出结果是什么?
输出结果:
1. string length =2
1. string bytes length =6
1. string char length =2
2. string length =4
2. string bytes length =8
2. string char length =4
3. string length =3
3. string bytes length =7
3. string char length =3
我们知道,String.getBytes()如果不指定编码格式,Java会使用操作系统的编码格式得到字节数组,在我的MacOS中,默认使用UTF-8作为字符编码(locale命令可以查看操作系统的编码),所以在我的机器运行,String.getBytes()会返回UTF-8编码的字节数组。
String.length返回代码单元的长度(UTF-16编码)。
String.toCharArray返回字符数组。
Unicode
Unicode解决了各国自行一套的问题,将世界上所有的符号都纳入其中。它符提供了唯一码点,不论是什么平台、不论是什么程序、不论是什么语言。
- 码点code point范围从 0x0 - 0x10FFFF,共分为17个Plane,每个Plane中有65536个字符,共可容纳:
17*(16*16*16*16)= 1114112
个字符。 - 第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP)。其他平面称为辅助平面(Supplementary Planes, SP),或astral Plane。
- BMP内,从U+D800到U+DFFF之间的码位区块是永久保留不映射到Unicode字符。后面介绍的UTF-16就利用保留下来的0xD800-0xDFFF区段的码位来对辅助平面的字符的码位进行编码。
我们设置的字符串都是两个unicode字符,输出结果:
- 普通的中文字:字符串的长度是2,每个中文字按UTF-8编码是三个字节,字符数组的长度看起来也没问题
- emojis字符:我们设置了两个emojis字符,男女头像。结果字符串的长度是4, UTF-8编码8个字节,字符数组的长度是4
- 生僻的中文字:我们设置了两个中文字,其中一个是生僻的中文字。结果字符串的长度是3, UTF-8编码7个字节,字符数组的长度是3
看起来字符串的字符数和我们预期的有点不一样,我们的字符串只有两个unicode字符, 可是输出结果有时候是2,有时候是3, 有时候是4,为什么呢?
不管为什么,这至少说明了java中表示一个字符,某些字符使用一个char,但是某些字符使用两个char。也就说明就,java肯定不可能只能表示6万多个字符。
增补字符
Unicode码代码点为U+0000到U+10FFFF,一共1114112个码位,其中U+0000 到U+FFFF的部分被称为基本多语言面(Basic Multilingual Plane,BMP)。U+10000及以上的字符称为增补字符。在Java中(Java1.5之后),增补字符使用两个char型变量来表示。第一个char型变量的范围称为“高代理部分”(high-surrogates range,从"uD800到"uDBFF,共1024个码位), 第二个char型变量的范围称为low-surrogates range(从"uDC00到"uDFFF,共1024个码位),这样使用surrogate pair可以表示的字符数一共是1024的平方计1048576个,加上BMP的65536个码位,去掉2048个非法的码位,正好是1,112,064个码位。
有可能你会问, 对于一个UTF-16编码的扩展字符,它以4个字节来表示,那么前两个字节会不会和BMP平面冲突,导致程序不知道它是扩展字符还是BMP平面的字符?
其实是不会的, 幸运的是, 在BMP平面中, U+D800到U+DFFF之间的码位是永久保留不映射到Unicode字符,UTF-16就利用保留下来的0xD800-0xDFFF区块的码位来对辅助平面的字符的码位进行编码。
UTF-16编码中,辅助平面中的码位从U+10000到U+10FFFF,共计FFFFF个,需要20位来表示。第一个整数(两个字节,称为前导代理)要容纳上述20位的前10位,第二个整数(称为后尾代理)容纳上述20位的后10位。
前导代理的值的范围是0xD800到0xDBFF,后尾代理的0xDC00~0xDFFF。可以看到前导代理和后尾代理的范围都落在了BMP平面中不用来映射的码位,所以不会产生冲突,而且前导代理和后尾代理也没有重合。
这样我们得到两个字节的,就可以直接判断它是否是BMP平面的字符,还是扩展字符中的前导代理还是后尾代码。
import java.io.*;
class TestSup
{
public static void main(String[] args) throws IOException
{
int[] codePoints = {0xd801,0xd802,0xdf00,0xdf01,0x34};
String str = new String(codePoints,0,5);
char[] ch = str.toCharArray();
for(char c:ch){
System.out.print(c+"--"+Integer.toHexString(c)+" ");//输出???,因为Unicode中不存在这样的char
}
/*测试能否写入文件*/
FileWriter out = new FileWriter("aa");
out.write(ch);
out.close();
System.out.print("\n***********************\n");
FileReader in = new FileReader("aa");
int c;
/**
*对比结果发现非代理范围的字符可以正常写入与读出,但是来自高代理与低代理范围的
*字符无法正常写入,而是被转化为0x3f
*/
while((c = in.read()) != -1){
System.out.print(Integer.toHexString(c)+" ");//为什么是3f?
}
in.close();
System.out.println(str);
}
}
可以得出:如果要向文本文件写入或读出增补字符,只能采用stream的方式读写。读出后根据代理范围进行判断,是否是增补字符(需要考虑编码)。比如是utf-16编码,需要根据高低代理范围进行判断。
对于char类型来说,charAt(int index)只能获取BMP的字符,对于增补字符,是无法正常获得的.所以当字符串中包含增补字符又该如何获取呢,当文档当中有增补字符呢?
public static void main(String[] args) throws Exception{
// 构造一个高代理部分和底代理部分
int[] codePoints = {0xd899,0xdc99};
String s = new String(codePoints,0,2);
// 可以发现只输出了一个字符,也就是一个增补字符
System.out.println("s: " + s);
// 说明length()是按代码单元计算的
System.out.println("s.length: " + s.length());
// 输出结果是两个代码单元的值,不能正确的获取到字符
System.out.println((int)s.charAt(0)+" "+(int)s.charAt(1));
// 返回的是一个代码点的值
System.out.println(s.codePointAt(0));
}
方法介绍
String类中的codePointAt(int index)
Character.isSupplementaryCodePoint(int codePoint)
java.lang.Character.toChars(int codePoint)
IntStream codePoints()
int codePointCount(int beginIndex, int endIndex)
1.String类中的codePointAt(int index)
若index所指的为BMP(基本多文种平面或平面0)的索引,则直接返回该代码点值,
否则,当index所指的为增补字符的索引:
1.索引指定的char值属于高代理项范围,则返回该增补字符的代码点值.
2.索引指定的char值属于低代理项范围,则返回该增补字符的低代理项的代码点值.也就是把低代理项当成独立的项来看待了
2.Character.isSupplementaryCodePoint(int codePoint)
确定指定字符(Unicode 代码点)是否在增补字符范围内。
3.java.lang.Character.toChars(int codePoint)
指定字符(Unicode代码点)存储在一个UTF-16表示形式转换的字符数组。
如果指定的代码点为BMP(基本多文种平面或平面0)的值,由此产生的char数组具有相同的值码点。
如果指定的代码点是一个增补代码点,由此产生的char数组具有相应的代理对。
4.IntStream codePoints()
返回所有代码点的值的int类型的流,他可以判断出一个字符串中正确的字符的个数,而不是代码单元。
4.int codePointCount(int beginIndex, int endIndex)
返回下标范围内的代码点个数。注意:这里的下标就是字符数组下标
自 Java 1.5 java.lang.String就提供了Code Point方法, 用来获取完整的Unicode字符和Unicode字符数量