Java文件IO流和字符编码
2013年02月21日

注:本文是《大数据量报表技术研究》专著的一部分


一、问题的提出

w  Java有哪些基础的IO流?各有什么特点?

w  如何读写文本文件?考虑效率和编码问题。

w  如何读写二进制文件?考虑大文件问题和效率问题。

 

二、问题的研究

JavaAPI里面,IO流最基础的是InputStreamOutputStreamReaderWriter,前两种是基于字节(byte)输入输出设计的,后两种是基于字符(charUCS-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},一个char16 bite中的前8bite和后8bite),如果知道这种对应关系,这几者之间是可以互相转换的。

最初的Unicode编码是固定长度的,16位,也就是2两个字节代表一个字符,这样一共可以表示65536个字符。但是,要表示所有的字符是远远不够的。因此Unicode 4.0定义了一组附加字符编码,附加字符编码采用216位(4个字节)来表示,这样最多可以定义1048576(百万+)个附加字符。

Unicode只是一个编码规范,Unicode的实现方式有很多种,例如UTF-8GB2312UTF-16等等。实际上现在用得最多的还是16位的Unicode编码(UCS-2),因为6万多个字符已经足够包括各种语言的常用字符了(GB23126763个汉字,GBK2万多个汉字而已)。

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。即所有的ReadersWriters 默认用的都是这个编码。

 

w  关于UTF-8UTF-16GBKGB2312编码的选择

中文字符这四种编码格式都能处理,GB2312GBK编码规则类似,但是GBK 范围更大,它能处理所有汉字字符,所以 GB2312GBK比较应该选择GBKUTF-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 WriterBufferedWriterOutputStreamWriter

将字符写入文件,用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要进行1000000IO操作。但是用BufferedWriter,假设缓冲大小为1024,则只需要 1000000/1024 IO

但是如果这个 str 本身就有2000个字节,那么用WriterBufferedWriter其实效果都差不多了。(这个原理在下文讲OutputStream时会用到)

另外,注意到OutputStreamWriter这个类,它是字符流通向字节流的桥梁。一般情形下,只要涉及到非英文字符,就需要使用OutputStreamWriter进行编码转换。

总结:一是使用BufferedWriter,二是使用OutputStreamWriter

 

3OutputStreamInputStream

对于字节流的读写,通常是使用InputStreamOutputStream。因为读写字节流的长度一般是由我们自己控制的,所以通常我们不需要用BufferedOutputStreamBufferedInputStream,而是使用自定义的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等,这样就能达到和BufferedOutputStreamBufferedInputStream一样的效果。

 

4、其他更高级的IO

其他高级的IO流,使用得比较少,本文不做介绍。

只提一点:关于Javanionew 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