Java 中日常使用的 IO 流总结
在 Java IO 流体系中提供了近 40 个类,这些类看起来非常乱又没有规律,所以我之前一直很抵触 IO 流,我觉得太乱了。但是在写代码的时候呢,又会经常要去使用,而且经常又用错...
所以这次花时间去重新复习了一遍,经过总结发现我们日常使用到的 IO 流其实就那几个,就能满足我们的日常需求,在这里写一些总结,方便日后复习。
一张图片就能看出他们之间的关系:
概述:
- 从大的方面分为两大类:字节流和字符流
- 字节流主要用于读取诸如图像数据、音频数据之类文件的原始字节流。
- 字符流主要用于读取字符文件
- BufferedWriter 和 BufferedReader 也叫缓冲流 ,用于更高效地从字符流进行读写。缓冲各个字符,从而实现字符、数组和行的高效读取。
- PrintStream 和 PrintWriter 叫打印流,PrintStream 可以输出到控制台、文件、网络等多种目的地,而 PrintWriter 只能输出到文件或字符输出流。
【应用总结】
总的来说,对于读取/写入图像、音频等非字符文件,使用字节流 FileInputStream / FileOutputStream
要读取/写入字符文件,一律使用缓冲流 BufferedReader / BufferedWriter ,可以实现高效IO操作
要输出到控制台、网络等目的地,使用打印流 PrintStream / PrintWriter
节点流
所有节点流都是直接以物理 IO 节点作为构造器参数的,例如 FileInputStream fis = new FileInputStream("obj.txt");
首先是最原始的 4 个节点流接口:InputStream
、OutputStream
、Reader
、Writer
。
以及他们的常用的直接实现类:FileInputStream
、FileOutputStream
、FileReader
、FileWriter
。
这 4 个是最常用的,不多介绍,这里举个文件复制的例子:
import java.io.*;
/**
* 将 aa.txt 的内容 复制到 bb.txt
*/
public class FileCopyDemo {
public static void main(String[] args) {
try {
// 创建流
FileInputStream fis = new FileInputStream("aa.txt");
FileOutputStream fos = new FileOutputStream("bb.txt");
byte[] buff = new byte[256];
int hasRead = 0;
// 读取 fis 的内容到 buff中,每次最多读取 256 字节
while ((hasRead = fis.read(buff)) > 0){
fos.write(buff);
}
//关闭流
fos.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
字节流和字符流的功能类似,只不过字符流只能操作字符、文本文件,而不能操作图片、音频等文件。
字符流的文件复制案例:
import java.io.*;
/**
* 字符流的文件复制
*/
public class Demo2 {
public static void main(String[] args) {
try {
//创建字符流
FileReader fis = new FileReader("aa.txt");
FileWriter fos = new FileWriter("bb.txt");
//操作的是字符
char[] buff = new char[256];
int hasRead = 0;
while ((hasRead = fis.read(buff)) > 0){
fos.write(buff);
}
//关闭流
fos.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
节点流的基本概念模型如下图。是每一次获取数据是通过一个节点去获取。
处理流
对比节点流,处理流是 ”嫁接“ 在已存在的流的基础之上,允许应用程序采用相同的代码、透明的方式来访问不同的输入/输出设备的数据流。
【为什么使用处理流】
归纳起来就是两点:
- 对于开发人员来说,处理流输入/输出操作更简单;
- 使用处理流的执行效率更高。
通过使用处理流,Java 程序无须理会输入/输出节点是磁盘、网络还是其他设备,程序只要将这些节点流包装成处理流,就可以使用相同的输入/输出代码来读写不同 IO 设备的数据。
使用处理流的典型思路是,使用处理流来包装节点流,程序通过处理流来执行输入/输出功能,让节点流与底层的 I/O 设备交互。
识别处理流的方法很简单,只要流的构造器参数不是一个物理节点,而是一个已经存在的流,这种流就一定是节点流。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
/**
* 使用 PrintStream 处理流来包装 OutputStream
*/
public class PrintStreamTest {
public static void main(String[] args) {
try {
// 创建字节输出流
FileOutputStream fos = new FileOutputStream("aa.txt");
// 包装成处理流
PrintStream ps = new PrintStream(fos);
//下面这种方式效率更高
// PrintWriter ps = new PrintWriter(new BufferedWriter(new FileWriter("aa.txt")));
// 使用 PrintStream 执行输出
ps.println("这个会输出到文件");
// 直接将这个对象输出到文件
ps.println(new PrintStreamTest());
} catch (IOException e) {
e.printStackTrace();
}
}
}
上面使用到一个处理流 PrintStream,它的输出功能非常强大,标准输出 System.out 的类型就是 PrintStream。
通常如果需要输出文本内容,都应该包装成 PrintStream 后进行输出。
与之对应的还有一个处理字符输出流的处理流 PrintWriter。
常用的处理流还有 BufferedWriter
、BufferedReader
,这两个也叫缓冲流,后面的例子会使用到。
转换流
IO 体系中提供了两个转换流:
-
InputStreamReader
:将直接输入流转换成字符输入流 -
OutputStreamWriter
:将字节输出流转换成字符输出流
IO 转换流只有 字节流 ----> 字符流
【为什么】
因为字节流适用范围更广,但字符流操作更方便。如果一个流已经是字符流了,是一个用起来更方便的流,为什么要转换成字节流呢?反正,如果一个字节流,但可以明确知道这个流的内容是文本,那么将它转换成字符流来处理会更方便一些。
所以 Java 只提供了将
字节流
转换成字符流
的转换流。
下面举个例子。Java 使用 System.in 代表标准输入流,即键盘输入。但这个标准输入流是 InputStream 类的实例,使用起来不方便,而且键盘输入的内容都是文本内容,所以可以使用 InputStreamReader 将其转换成字符输入流。
import java.io.*;
/**
* 转换流使用,将键盘输入的内容写入文件 bb.txt
*/
public class BufferTest {
public static void main(String[] args) {
try {
// 将 System.in 对象转行成 Reader 对象
InputStreamReader reader = new InputStreamReader(System.in);
// 将普通的 Reader 包装成处理流 BufferedReader
BufferedReader br = new BufferedReader(reader);
// 创建输出流,并转换成字符流 Writer
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("bb.txt",true));
String line= null;
// 采用循环读取键盘输入的内容
while ((line = br.readLine()) != null) {
if (line.equals("quit")){
System.exit(1);
}
// 写入输出流
writer.write(line);
// 换行
writer.write('\n');
// 刷新输出流,写入文件
writer.flush();
System.out.println("输入的内容为:" + line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里还用到了 BufferedReader,将普通的 Reader 再次包装成处理流 BufferedReader,利用它的 readLine() 方法一次可以读取一行内容。
【提示】
BufferedReader 具有一个 readLine() 方法,可以非常方便地一次读入一行内容,所以经常把读取文本内容的输入流包装成 BufferedReader ,用来方便地读取输入流的文本内容。
缓冲流
缓冲流是从字符流中读取/写入文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
可以指定缓冲区的大小,或者可使用默认的大小。大多数情况下,默认值就足够大了。
BufferedReader
通常,Reader 所作的每个读取请求都会导致对底层字符或字节流进行相应的读取请求。因此,建议用 BufferedReader 包装所有其 read() 操作可能开销很高的 Reader(如 FileReader 和 InputStreamReader)。
例如, 将缓冲指定文件的输入:
BufferedReader in = new BufferedReader(new FileReader("foo.in"));
如果没有缓冲,则每次调用 read() 或 readLine() 都会导致从文件中读取字节,并将其转换为字符后返回,而这是极其低效的。
BufferedWriter
该类提供了 newLine() 方法,它使用平台自己的行分隔符概念,此概念由系统属性 line.separator
定义。并非所有平台都使用新行符 ('\n') 来终止各行。因此调用此方法来终止每个输出行要优于直接写入新行符。
通常 Writer 将其输出立即发送到底层字符或字节流。除非要求提示输出,否则建议用 BufferedWriter 包装所有其 write() 操作可能开销很高的 Writer(如 FileWriters 和 OutputStreamWriters)。例如,将缓冲 PrintWriter 对文件的输出。
PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));
如果没有缓冲,则每次调用 print() 方法会导致将字符转换为字节,然后立即写入到文件,而这是极其低效的。
所以前面处理流的例子中,用第二种方式创建处理流效率更高。
下面例子将键盘输入的内容输出到文件:
import java.io.*;
/**
* 缓冲流
*/
public class BufferedWriterTest {
public static void main(String[] args) {
try {
//输出流
FileOutputStream fos = new FileOutputStream("aa.txt", true);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos));
//创建输入流, 从键盘输入
InputStreamReader reader = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(reader);
String line = null;
while (!(line = br.readLine()).equals("quit")) {
writer.write(line);
writer.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}