前言:最近,想写一篇关于介绍产生”乱码问题“根本原因的文章,因此,查看了Java中的字符是如何存储的,即char数据类型。在此将学到的知识做一个总结。
一、char数据类型
char类型最初用于表示Unicode字符集中的一个字符,但是随着Unicode标准的不断发展,其字符集不断扩展,表示的字符随之增加,已经超出了16位的char类型可以表示的范围(65535),现今,char类型用于表示一个代码单元。有些Unicode字符需要使用一个代码单元表示,有些Unicdoe字符需要两个代码单元表示。关于代码单元的概念,下文会详细介绍,这里先有一个印象即可。
1. 字面量值
字面量值需要单引号''
括起来,单引号中必须有值,其形式如下:
- 单个Unicode字符,最常见的形式,例如,
'A'
、'中'
; - 转义序列\u,范围\u0000~\uFFFF,例如,
'\u2122'
表示字符™
,'\u03C0'
表示字符'π'
; - 特殊字符的转义序列,例如
'\n'
表示换行,'\''
表示单引号。
转义字符,一般是不可打印的或者与语言的语法字符产生了冲突。
二、Unicode字符集
1. 字符集和编码规则
我们经常会听到Unicode、UTF-8、UTF-16这些术语,然而,它们是完全不同的概念。Unicode是字符集,UTF-8、UTF-16是编码规则,具体概念如下:
字符集:为每一个「字符」分配了一个唯一的 ID或者编号(称为为码位 / 码点 Code Point)
编码规则:将「编号」转换为字节序列的规则
例如,‘中’ 的码点如下
其使用不同编码规则,进行编码的结果如下
2. Unicode字符集
Java设计之初Unicode字符集中的字符个数还不到65536的一半,所以使用16位的char类型完全可以表示所有字符,但是,随着中、日、韩等其它字符的加入,导致Unicode字符集中的字符个数超过了65535,此时char类型已经无法表示Unicode字符集中的所有字符了。
我们先介绍一些必备的基础知识。
码点表示了一个字符的ID或者编号,在Unicode标准中,采用十六进制书写,并加上前缀U+,例如U+0041(转换为十进制即65),表示字母A的码点。
把Unicoee字符分为17个平面,每个平面包含不同种类的字符。
基本的多语言平面,码点范围U+0000~U+FFFF,表示了我们各个国家常用的字符,也是开始的char类型可以表示的字符。
其他平面(1到16号平面),码点范围U+10000~U+10FFFF,表示辅助字符,例如我们的不常用的繁体字等等,char类型无法直接表示这些字符,例如,char = '‘𝕆’ 会编译报错( 𝕆,一个数学符号,Unicdoe字符码点 U+1D546)
那么,char如何表示码点在U+10000~U+10FFFF之间的Unicode字符呢?
它借鉴了UTF-16编码规则。
UTF-16使用不同长度的编码表示所有Unicode字符。在基本的多语言平面中,使用16位表示一个字符,称为代码单元,而其他平面的辅助字符需要使用32位,也就是一对代码单元表示一个字符。
U+D800~U+DBFF表示第一个代码单元,U+DC00~U+DFFF表示第二个代码单元。
U+D800~U+DFFF,这一段码点称为替换区域,可以看出它属于基本的多语言平面(U+0000~U+FFFF)的范围,为了避免产生歧义或冲突,替换区域的码点没有没配给任何字符。
假设为替换区域码点分配了字符,就无法判断该码点是表示一个字符,还是辅助字符的第一个代码单元或第二个代码单元。
具体让我们看一下如何表示字符 ‘⑪’ (Unicdoe字符码点 U+1D546)?
第一个代码单元:D800到DBFF
1101 1000 0000 0000
1101 1011 1111 1111
110110XXXXXXXXXX 可以编码10位二进制数
第二个代码单元:DC00到DFFF
1101 1100 0000 0000
1101 1111 1111 1111
110111XXXXXXXXXX 可以编码10位二进制数
U+10000到U+10FFFF 可以看出转换成二进制树最多有21位,但是我们加起来最多可以编码20位二进制数,怎么办?
将范围偏移-10000,范围变成了U+0000到U+FFFFF,如此,就满足20位编码了。
⑪ U+1D546 偏移后U+D546
1101 0101 0100 0110
后十位“01 0100 0110"放入第二个代码单元110111XXXXXXXXXX:1101 1101 0100 0110,对应的十六进制数:DD46
前十位(不足十位首部补0)00001101 01 放入第一个代码单元110110XXXXXXXXXX:1101 1000 0011 0101,对应的十六进制数是:D835
因此,⑪,U+1D546使用两个代码单元表示:D835 DD46
我们在程序中如何存储码点在U+10000到U+10FFFF之间的字符呢?
直接存储字符字面量值或者转义序列\u,编译报错。很容易理解char类型16位正好只能存储一个代码单元,而该字符需要两个代码单元表示,无法用char类型直接表示该字符。
总结:现在,char类型用于描述一个代码单元。对于基本的多语言平面中的字符(码点范围U+0000到U+FFFF,不包含替换区域)可以使用一个代码单元表示,也就是一个char值。对于其他平面的字符(码点范围U+10000到U+10FFFF)可以使用两个代码单元表示,也就是两个char值。
严格来说,char类型可以表示使用一个代码单元表示的Unicode字符,char类型无法直接表示两个代码单元表示的字符。
怎么办,对于需要两个代码单元表示的字符就不能使用了吗?
不是的,我们可以把他们放在字符串类型中,字符串是由字符构成的,它把U+D800到U+DBFF之间的代码单元解读为字符的第一个代码单元,把U+DC00到U+DFFFF解读为字符的第二个代码单元,然后把两个代码单元解读为一个字符。它把U+0000到U+FFFF之间除去替换区域(U+D800到U +DFFF)的代码单元解读为一个字符。
String str = "\uD835\uDD46";
最后的建议,程序中最好不要使用char类型,除非你想处理代码单元。像操作Unicode字符,可以使用Java中的String类型。
public static void main(String[] args) {
// 字符串中放入需要两个码点表示的字符'𝕆'
String str = "𝕆A";
// 获取索引为0的代码单元,而不是字符
char ch0 = str.charAt(0);
char ch1 = str.charAt(1);
char ch2 = str.charAt(2);
System.out.println(ch0);
System.out.println(ch1);
System.out.println(ch2);
}
有些字符不能被正确的处理