注:本文是《大数据量报表技术研究》专著的一部分
一、问题的提出
w Java有哪些基础的IO流?各有什么特点?
w 如何读写文本文件?考虑效率和编码问题。
w 如何读写二进制文件?考虑大文件问题和效率问题。
二、问题的研究
在Java的API里面,IO流最基础的是InputStream、OutputStream、Reader、Writer,前两种是基于字节(byte)输入输出设计的,后两种是基于字符(char,UCS-2)输入输出设计的。
1、编码知识
Java的字符(char)是采用的UCS-2编码方式,占2个字节。
UCS-2:即16位、2个字节的Unicode编码,范围为0 to 65535 (0x00-0xffff)。
例如'余'字,它的Unicode编码为"4f59",转换成int等于20313,转换成byte[]为{79, 89}(即{0X4f, 0X59},一个char的16 bite中的前8个bite和后8个bite),如果知道这种对应关系,这几者之间是可以互相转换的。
最初的Unicode编码是固定长度的,16位,也就是2两个字节代表一个字符,这样一共可以表示65536个字符。但是,要表示所有的字符是远远不够的。因此Unicode 4.0定义了一组附加字符编码,附加字符编码采用2个16位(4个字节)来表示,这样最多可以定义1048576(百万+)个附加字符。
Unicode只是一个编码规范,Unicode的实现方式有很多种,例如UTF-8,GB2312,UTF-16等等。实际上现在用得最多的还是16位的Unicode编码(UCS-2),因为6万多个字符已经足够包括各种语言的常用字符了(GB2312有6763个汉字,GBK有2万多个汉字而已)。
Java中字符串的默认编码,是在虚拟机启动时决定,通常根据语言环境和底层操作系统的Charset来确定。可以用如下方法获取当前JVM的默认编码:
java.nio.charset.Charset.defaultCharset();
注意,这个默认编码是在虚拟机启动时便决定了的,只初始化一次,运行中不能改变。如果想修改这个编码,可以在JVM的启动参数中设置如下:
-Dfile.encoding="UTF-8"
例如eclipse,设置方法为:点击鼠标右键,Run->RunConfigurations...,可以在Arguments->VM arguments中设置,也可以在Common->Encoding中设置。
This property is used for the default encoding in Java, all readers and writers would default to using this property。即所有的Readers和Writers 默认用的都是这个编码。
w 关于UTF-8、UTF-16、GBK、GB2312编码的选择
中文字符这四种编码格式都能处理,GB2312与GBK编码规则类似,但是GBK 范围更大,它能处理所有汉字字符,所以 GB2312与GBK比较应该选择GBK。UTF-16 与 UTF-8 都是处理 Unicode 编码,它们的编码规则不太相同,UTF-16 编码效率最高,字符到字节相互转换更简单,进行字符串操作也更好,但是容错性差。它适合在本地磁盘和内存之间使用,可以进行字符和字节之间快速切换,如 Java 的内存编码就是采用 UTF-16 编码。但是它不适合在网络之间传输,因为网络传输容易损坏字节流,一旦字节流损坏将很难恢复,相比较而言 UTF-8 更适合网络传输,对 ASCII 字符采用单字节存储,另外单个字符损坏也不会影响后面其它字符,在编码效率上介于 GBK 和 UTF-16 之间,所以UTF-8在编码效率上和编码安全性上做了平衡,是理想的中文编码方式。
w 乱码问题和编码的转换
客户用的GBK编码,而我方用的UTF-8,怎么办?
首先当然考虑设置问题,比如html中、tomcat的设置,
各种编码之间不一定兼容,所以才造成了乱码,例如在互联网上被广泛使用的默认编码ISO_8859_1,它只能表示256个字符(0-255),如果将UTF-8字符转换成ISO_8859_1,势必会乱码。
2、 Writer、BufferedWriter和OutputStreamWriter
将字符写入文件,用Writer,通过OutputStreamWriter包装类,将字符流转换成字节流。例如:
Writer outWrite = null;
OutputStream stream = new FileOutputStream(file);
outWrite = new OutputStreamWriter(stream, "GBK");
outWrite.write(str); // 通过OutputStreamWriter将字符str输入到文件file中
但是,Writer效率是比较低的,它每执行一个write方法,都要等前一个write方法的IO写操作完成后,才能继续。由于IO读写速度远远低于CPU速度和内存读写速度,所以该方法会极大的受限于IO。因此,我们一般用BufferedWriter代替,BufferedWriter有一个自动缓冲的机制,当缓冲区满了之后,才输出到IO。示例如下:
BufferedWriter outWrite = null;
OutputStream stream = new FileOutputStream(file);
OutputStreamWriter osw = new OutputStreamWriter(stream, "GBK");
outWrite = new BufferedWriter(osw);
outWrite.write(str); // 通过BufferedWriter将字符str输入到文件file中
outWrite.flush();
out.close();
BufferedWriter的使用原则是:
“it is advisable to wrap a BufferedWriter around any Writer whose write() operations may be costly, such as FileWriters and OutputStreamWriters.”(摘自Java API)。
也就是说,只要是频繁的、大量的字符流写入,都应该使用BufferedWriter。但是,如果每次写入的字节长度是自己设置的,则用Writer就可以模拟BufferedWriter的效果。例如:
for(int i=0;i<1000000;i++){
writer.write( str ); // 或者 bufferedWriter.write( str )
}
如果这个 str 只有10个字节,则用Writer要进行1000000次IO操作。但是用BufferedWriter,假设缓冲大小为1024,则只需要 1000000/1024 次IO。
但是如果这个 str 本身就有2000个字节,那么用Writer和BufferedWriter其实效果都差不多了。(这个原理在下文讲OutputStream时会用到)
另外,注意到OutputStreamWriter这个类,它是字符流通向字节流的桥梁。一般情形下,只要涉及到非英文字符,就需要使用OutputStreamWriter进行编码转换。
总结:一是使用BufferedWriter,二是使用OutputStreamWriter。
3、OutputStream和InputStream
对于字节流的读写,通常是使用InputStream和OutputStream。因为读写字节流的长度一般是由我们自己控制的,所以通常我们不需要用BufferedOutputStream、BufferedInputStream,而是使用自定义的buffer数组。例如,拷贝文件的方法,如下:
public static long copyLarge(InputStream input, OutputStream output, byte[] buffer)
throws IOException {
long count = 0;
int n = 0;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
return count;
}
每次读取固定长度的字节,buffer的长度一般是512的倍数,比如1024,、8192等,这样就能达到和BufferedOutputStream、BufferedInputStream一样的效果。
4、其他更高级的IO流
其他高级的IO流,使用得比较少,本文不做介绍。
只提一点:关于Java的nio(new io),经过笔者较深入地测试,对文件流的读写效果不是很明显,有兴趣的读者可以自己去研究。
5、补充知识点:对文件进行打包和压缩
网上流传有两种方式:
1)第一种方式,采用JDK API中自带的类:
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
缺点:不支持中文文件名。压缩过程中遇到中文文件名,则文件名会乱码,解压过程中遇到中文文件名,则会报错。
2)第二种方式,利用Apache ANT中的工具类:
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipOutputStream;
优点:支持自定义编码,可以解决压缩的文件名的中文乱码问题。
缺点:这个工具类在ant.jar中,使用时需要额外引入这个jar文件。
我根据JDK API重写了ZipOutputStream相关的类,解决了编码问题,使得能支持多种字符编码方式。具体例子见附件的Demo。