版权声明:本文由 Hov 所有,发布于 http://chenhy.com ,转载请注明出处。
0 写在前面
IO 操作是任何编程语言都无法回避的问题,因为 IO 操作是机器获取和交换信息的主要途径。 Java 的 IO 是以流为基础进行输入输出的,所有的数据被串行化写入输出流或从输入流读入。 java.io 提供了全面的 IO 接口,包括文件读写、标准设备输出等。
注:流是一个很形象的概念。当程序需要读取数据时会开启一个通向数据源的流,这个数据源可以是文件、内存或网络连接。同样,当程序需要写入数据时会开启一个通向目的地的流,而数据就在数据源和目的地之间“流动”。
下图为 Java IO 体系的组成结构。由图可知, Java IO 主要分为字节流和字符流,其中字节流以 InputStream 和 OutputStream 向下继承,字符流以 Reader 和 Writer 向下继承。
本文将详细讲解字节流和字符流,并简要介绍随机存取文件 RandomAccessFile 和 Apache 的 IO 类库。
1 预备知识
在讲解 IO 相关的知识前,我们有必要了解一下字节、字符、编码等概念。毕竟 IO 操作实质上是基于字节、字符来实现的。
1.1 字节
字节是通过网络传输信息或硬盘、内存存储信息的单位,也是计算机用于计量存储容量和传输容量的一种计量单位。一个字节等于 8 位二进制,是一个很具体的存储空间,如 0x01,0x20 。
1.2 字符
字符是人们使用的记号,抽象意义上的一个符号。如“1”,“a”,“¥”,“好”等。实质上,字符是由字节组成的,一个字符可以由一个或多个字节组成。
1.3 字符集
不同国家和地区所制定的不同 ANSI 编码标准中,都只规定了各自语言所需的字符。汉字标准(GB2312)就是一个字符集,它规定的是汉字的编码标准。
字符集(编码)有两层含义:
- 使用哪些字符,也就是哪些字符、汉字或符号会被收入标准中,所包含“字符”的集合就叫做“字符集”;
- 规定每个字符分别用一个字节还是多个字节存储,用哪些字节存储,这个规定就叫做编码。 字符集和编码一般同时制定,字符集除了有“字符集合”含义外,还包含了“编码”的含义。
值得注意的是,为了使国际间信息交流更方便,国际组织制定了 UNICODE 字符集,它为各种语言中的每一个字符设定了统一且唯一的数字编号,以满足跨平台、跨语言进行文本交换、处理的要求,如我们熟悉的 UTF-8 。
2 字节流与字符流
Java IO 流可分为字节流和字符流两大类,两种流都可以实现输入输出操作。其中,字节流继承自 InputStream 和 OutputStream ,字符流继承自 Reader 和 Writer 。
2.1 字节流
字节流可以处理所有类型的数据,如图片、视频、文字等。字节流每读取一个字节就返回一个字节,字节流在 Java 中对应的类通常以“Stream”结尾。
2.1.1 字节输入流:InputStream
我们可以通过 InputStream 读取字节数据。 InputStream 类的定义如下:
public abstract class InputStream extends Object implements Closeable{
......
}
InputStream 是一个抽象类,这意味着其必须依靠其子类实现具体的功能。比如从文件中读取数据,我们可以用 FileInputStream 来实现, FileInputStream 就是 InputStream 的一个子类。当然, InputStream 下面还有其它的子类,如缓冲输入流 BufferedInputStream 。
(1)通过 FileInputStream 读取文件数据
/* ReadByte.java */
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ReadByte {
public static void main(String[] args) {
try {
//计时开始
long times = System.currentTimeMillis();
//实例化FileInputStream对象
File file = new File("Data.zip");
FileInputStream fis = new FileInputStream(file);
//读取文件内容
byte[] input=new byte[500];
int count = 0; //记录读取次数
while(fis.read(input)!=-1){
count++;
}
//记得关闭流
fis.close();
//计时结束
System.out.println("用时: "+ (System.currentTimeMillis()-times) + "ms");
System.out.println("读取次数: "+count);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
程序运行结果:
用时: 117ms
读取次数: 188206
(2)通过 BufferedInputStream 读取文件数据
前面已经通过 FileInputStream 读取了文件数据,接下来通过缓冲的方式来读取同样的文件。两者实现的效果一致,对比它们之间的效率。值得注意的是,下面的代码就比上面多出两行(实例化缓冲流和关闭缓冲流)。
/* BufferedRead.java */
import java.io.*;
public class BufferedRead {
public static void main(String[] args) {
try {
long times = System.currentTimeMillis();
File file = new File("Data.zip");
FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis); //实例化缓冲输入流
byte[] input=new byte[500];
int count = 0;
while(bis.read(input)!=-1){
count++;
}
bis.close(); //关闭缓冲流
fis.close();
System.out.println("用时: "+ (System.currentTimeMillis()-times) + "ms");
System.out.println("读取次数: "+count);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
程序运行结果:
虽然读取同样的次数,但缓冲的方式用时明显减少了。
用时: 52ms
读取次数: 188206
注意:缓冲区大小、读取文件的次数会影响读取效率。比如,减少缓冲区大小,读取次数将增多,缓冲的方式可效率更快;如果增加缓冲区大小,读取次数将减少,有无缓冲可能效率差别不大。
2.1.2 字节输出流:OutputStream
我们可以通过 OutputStream 输出字节数据。 OutputStream 类的定义如下:
public abstract class OutputStream extends Object implements Closeable,Flushable{
......
}
同 InputStream 一样,OutputStream 也是一个抽象类。要想使用 OutputStream ,必须通过子类实例化对象。同样,我们可以通过 OutputStream 的子类 FileOutputStream 往文件写入数据。
注:Closeable 表示可以关闭的操作,因为程序运行到最后必须关闭; Flushable 表示刷新,清空内存的数据。
(1)通过 FileOutputStream 往文件写入数据
/* WriteByte.java */
import java.io.*;
public class WriteByte {
public static void main(String[] args) {
try {
//实例化 FileOutputStream
File file = new File("OutputData.txt");
FileOutputStream fos = new FileOutputStream(file);
//往文件写入字符串数据
String str = "Hello China. 中文";
byte[] b = str.getBytes("UTF-8");
fos.write(b);
//关闭流
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
程序运行结果:
文件 OutputData.txt 将被写入数据 “Hello China. 中文”。
2.2 字符流
不同于字节流可以处理多种类型的数据,字符流仅能处理纯文本数据,如 txt 文本。字符流读取一个或多个字节时,先查找编码表,再将查到的字符返回。字符流在 Java 中对应的类通常以“Reader”和”Writer”结尾。
2.2.1 字符输入流:Reader
通过 Reader 以字符的方式读取数据, Reader 类的定义如下:
public abstract class Reader extends Objects implements Readable,Closeable{
......
}
(1)通过 InputStreamReader 读取数据
/* ReadChar.java */
import java.io.*;
public class ReadChar {
public static void main(String[] args) {
try {
//实例化 InputStreamReader 对象
File file = new File("text.txt");
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis);
//读取数据
char input[] = new char[100];
int l = 0; //一次读取了多少数据
while((l=isr.read(input))!=-1){
System.out.println(new String(input,0,l));
}
//关闭流
isr.close();
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
程序运行结果:
控制台将输出 text.txt 文件的内容。
2.2.2 字符输出流:Writer
通过 Writer 以字符的方式输出数据, Writer 类的定义如下:
public abstract class Writer extends Object implements Appendable,Closeable,Flushable{
......
}
(1)通过 OutputStreamWriter 往文件写入数据
/* WriteChar.java */
import java.io.*;
public class WriteChar {
public static void main(String[] args) {
try {
//实例化 OutputStreamWriter 对象
File file = new File("OutputData.txt");
FileOutputStream fos = new FileOutputStream(file);
OutputStreamWriter osw = new OutputStreamWriter(fos);
//往文件写入字符串数据
String str = "Hello China.";
osw.write(str);
//关闭流
osw.close();
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
程序运行结果:
字符串”Hello China.“将被写入 OutputData.txt 文件。
2.2.3 FileReader 和 FileWriter
同样,我们也可以通过 FileReader 和 FileWriter 来读写文件数据。
/* FileReaderWriter.java */
import java.io.*;
public class FileReaderWriter {
public static void main(String[] args) {
try {
//实例化 FileReader 对象
FileReader fr = new FileReader("text.txt");
BufferedReader br = new BufferedReader(fr);
//实例化 FileWriter 对象
FileWriter fw = new FileWriter("outputdata.txt");
BufferedWriter bw = new BufferedWriter(fw);
//从 Reader 读取数据,往 Writer 写入数据
String line = null;
while((line=br.readLine())!=null){
bw.write(line+"\n");
}
//刷新缓冲区
bw.flush();
//关闭流
br.close();
bw.close();
fr.close();
fw.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
程序运行结果:
文件 text.txt 中的内容将被写入 outputdata.txt 文件。
2.3 字节流与字符流的区别
前面我们已经介绍了字节流、字符流,并给出了具体的实现代码。当然,Java IO 并不仅限于上面提到的内容,但其它类的实现思路基本一致。字节流和字符流使用也非常相似,对比上面通过字节流和字符流分别从文件读取数据的代码你会发现,可在字节流的基础上包装成字符流来读取字符数据。当然,字节流和字符流还是有区别的:
字节流不使用缓冲区(内存),直接操作文件本身;字符流使用缓冲区。
字节流即使不关闭资源(close方法),也能输出内容;但字符流不使用 close 方法的话,则不会输出任何内容, 这正说明字符流使用的是缓冲区,这也是为什么我们要用 flush 方法强制刷新缓冲区,这样才能在不 close 的情况下输出内容。
那究竟用字节流好还是用字符流好呢?
字节流会比字符流更通用。字节流可以处理所有类型的数据,如视频、图片。字符流对视频或图片的读写就显得无能为力了,因为它只能处理纯文本数据。事实上,在硬盘上保存文件或进行传输时都是以字节的方式进行的,包括图片也是按字节完成,所以使用字节的操作是最多的。如果要通过 java 程序实现一个拷贝功能,显然应该选用字节流进行操作(因为可能拷贝的是图片),并且可采用边读边写的方式(节省内存)。
3 随机访问文件:RandomAccessFile
RandomAccessFile 唯一的父类是 Object ,这与其他流有所不同。它是一个完全独立的类,所有方法(绝大多数都只属于它自己)都是从零开始写的,它的行为与其它的 IO 类有些根本性的不同。
RandomAccessFile 一个很重要的特性就是它支持从文件的任意位置读写。
为了突出 RandomAccessFile 的特性,我们通过多个线程来操作同一个文件。
(1)通过线程往文件写入数据
/* WriteFile.java */
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
public class WriteFile extends Thread{
File file;
int block;
int L = 100;
public WriteFile(File f,int b){
this.file = f;
this.block = b;
}
@Override
public void run() {
try {
//实例化 RandomAccessFile 对象
RandomAccessFile raf = new RandomAccessFile(file,"rw");
raf.seek((block-1)*L); //文件写入位置
raf.writeBytes("\n"+"This is block "+block); //写入数据
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
(2)模拟多个线程同时写文件
/* MultiWriteFile.java */
import java.io.File;
public class MultiWriteFile {
static File file = new File("test.txt"); //文件
public static void main(String[] args) {
if(file.exists()){
file.delete();
}
//开启5个线程往同一个文件写入数据,注意写入顺序是 1、5、4、3、2
new WriteFile(file,1).start();
new WriteFile(file,5).start();
new WriteFile(file,4).start();
new WriteFile(file,3).start();
new WriteFile(file,2).start();
}
}
程序运行结果:
虽然写入顺序是 1、5、4、3、2,但它们都分别写入各自的区块位置,所以在文件中的顺序变回了 1、2、3、4、5。这就说明了 RandomAccessFile 可以从文件的任意位置写入数据,而不需要从头往后写数据。
注:后面的方框说明该区域没有数据,因为每个区块是100,我们只写入了15个数据。
(3)同样,我们可以读取文件中任意位置的数据
/* MultiWriteFile.java */
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
public class MultiWriteFile {
static File file = new File("test.txt");
public static void main(String[] args) {
try {
//实例化 RandomAccessFile 对象
RandomAccessFile raf = new RandomAccessFile(file,"r");
//文件读写位置:200
raf.seek(200);
//读取数据
byte[] str = new byte[16];
raf.read(str);
String in = new String(str);
System.out.println(in);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
程序运行结果:
成功从位置 200 处读取数据,输出 “This is block 3”。
4 Apache IO
前面我们已经讲解了 Java 中常用的 IO 操作,其实我们还可以通过外部的包来使用 IO。比如, Apache 提供了一个功能强大的包来实现 IO 操作,通过它可以大量减少我们的实现代码,操作十分方便。
Apache IO下载地址: http://commons.apache.org/proper/commons-io/download_io.cgi
下载完毕后,将 jar 文件导入工程就可以使用了。
下面简单展示如何使用 Apache IO 实现文件拷贝。当然,还有其它强大的功能,大家有兴趣可以查看官方文档如何使用。
(1)通过 Apache IO 实现文件拷贝
/* TestApacheIO.java */
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
public class TestApacheIO {
public static void main(String[] args) {
File file = new File("text.txt");
File newfile = new File("textnew.txt");
try {
//调用 FileUtils 的 copyFile 方法,实现文件拷贝
FileUtils.copyFile(file,newfile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
程序运行结果:
text.txt 文件将被拷贝到 textnew.txt 文件。
5 结语
- Java IO 可分为字节流和字符流两大类;
- 字节流能处理所有类型的数据,如视频、图片、文本;字符流只能处理纯文本数据;
- 字节流直接对文件操作,不需要缓冲区,直接输出内容;字符流需要缓冲区,刷新缓冲区才能输出内容。
- 随机访问文件 RandomAccessFile 支持从文件任意位置读写数据;
- Apache IO 提供了 IO 操作的简便接口。
参考资料: