java中的字符个数,增补字符

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字符数量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值