JAVA NIO(三):缓冲区的相互转换及中文乱码的解决方案
在Java IO中,Channel(通道)只能直接与ByteBuffer进行通信,这样我们可能会用ByteBuffer的视图来解决数据的转换问题,如将字符串转换为二进制缓冲区,整数缓冲区转换为二进制缓冲区,示例如下:
ByteBuffer buffer = ByteBuffer.allocate(100);
// 获取缓冲区的视图,但与ByteBuffer的mark、position、limit互相独立
CharBuffer charBuff = buffer.asCharBuffer();
// 更容易进行字符操作
charBuff.put("Hello, World!");
// 创建输出通道
WritableByteChannel outChannel = Channels.newChannel(System.out);
// 写入缓冲区数据
outChannel.write(byteBuf);
outChannel.close();
采用上述的方法,会导致以下几个问题:
1. 难以控制字符与字节的转换过程;
2. 难以获取缓冲区的当前位置与剩余空间;
3. 难以控制缓冲区的阶段读取,如20-50,50-70;
在上面的例子中,我们将CharBuffer与ByteBuffer进行了转换,极大地简化了字符的操作行为,但可惜的是,字符依赖于系统编码(这里是UTF-8,每个字符占两个字节),写入的时候每个字符占两个字节,但读取的时候却不知道编码,最后导致出现乱码。
要解决上述的问题,只有自己控制字符与字节转换的过程,才能保证字符的输入与输出保持一致,如下:
// 创建输出通道
WritableByteChannel outChannel = Channels.newChannel(System.out);
String text = "你好,JAVA!";
byte[] arr = text.getBytes("UTF-8");
ByteBuffer byteBuf = ByteBuffer.wrap(arr);
outChannel.write(byteBuf);
outChannel.close();
使用上述的方法,优点是能保证字符的正确转换,但是很显然,效率太低了,还有更好的办法吗?
经过实测,通过字节转换的效率并不低,并且比CharBuffer放入字符串的速度高4-5倍,对比数据如下:
字节数量 | ByteBuffer直接放入数据 | 通过CharBuffer放入数据 |
---|---|---|
1200 | PT0.000087808S | PT0.000437138S |
2400 | PT0.000099591S | PT0.000669771S |
4800 | PT0.000140644S | PT0.000880357S |
这里最有意思的地方在于,CharBuffer统一以两个字节存储一个字符,而ByteBuffer则依赖于编码,可能是两个字节,也可能是三个字节,最后导致512个字符,CharBuffer的limit为512,换算为ByteBuffer后为1024个字节,而直接用ByteBuffer存储字节则为1200(依赖于具体的字符,不定),如下:
字符数量 | ByteBuffer直接放入数据 | 通过CharBuffer放入数据 |
---|---|---|
512 | 1200 | 512 |
1024 | 2400 | 1024 |
2048 | 4800 | 1024 |
关于乱码,如果是以CharBuffer放入的数据,因为没有对应的字符集解码器,必定是乱码,只有通过同样的CharBuufer方式读出,才能正确解决,所以在这里,char是一种数据存储格式,就如同long数据、int数据一样,并不是操作系统上对应的字符集,所以无法解析。尤其是文本编辑器提示“此文档包含当前文本编码无法处理的字符”,则可以断定是以字符数据格式进行存储,处理方式如下:
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteBuffer bbuf = ByteBuffer.allocateDirect(1024);
long offset = 0;
long nums = 0;
while((nums = fc.read(bbuf, offset)) > 0) {
// 一定要反转回到正确位置,CharBuffer视图是基于当前位置创建的
bbuf.flip();
// 以字符数据方式进行读取
CharBuffer cbuf = bbuf.asCharBuffer();
bbuf.clear();
offset = offset + nums;
}
fc.close();
fis.close();
结论
- 一定要区分字符与字符数据,如果是操作系统上的字符,则可对应到文本编码,并有相应的字符集编码,而字符数据必定是二进制编码;
- 缓冲区存储的是字节(不是字符);
- 字符由多个字节组成,如果不能按照指定的顺序与数量读取所需的字节,必然出现乱码。