IO 流,掌控一切
上一篇文章我们认识了文件操作的源头 File 类,这篇文章就来聊聊文件操作的核心 IO 流。
我们经常可以听到:输入流、输出流、字节流、字符流、节点流、处理流等词语,咋一听,忍不住“哇~~~!”的一声,心里在想:“感觉好复杂的样子,学习 IO 流需要知道这么多东西啊!”,从而有了畏难的情绪。大家千万不要被这些词语吓到,望而却步,它们只不过是从三个维度对 IO 流的总结。
学习 IO 流是有套路的,通过这篇文章的学习,你一定能掌握 IO 流的使用技巧,从而掌控一切文件操作问题。
一、认识 IO 流
1、IO 流的分类
从 IO 流的流向来划分,IO 流分为:输入流、输出流。
从 IO 流要处理的数据来划分,IO 流分为:字节流、字符流。其中,字节流可以处理一切文件数据,包括纯文本,word文档,pdf文档,图片,音频和视频等二进制数据;字符流只能处理纯文本文件。
从 IO 流的功能来划分,IO 流分为:节点流和处理流。其中,节点流是用来包装数据源(File)的,它直接和数据源连接,表示从一个节点读取数据或者把数据写入到一个节点;处理流是用来包装节点流的,它是对一个已经存在的节点流进行连接,处理流通过增加缓存的方式来提高输入输出操作的性能。
总的来说,java.io 包中流的操作主要分为字节流和字符流两类,他俩都有对应的节点流与数据源进行连接,为了提高文件操作的性能,在节点流的基础上提供了处理流,以便增强节点流的功能,同时他俩都有输入和输出操作。
通过上面的分类,大家先对 IO 流先有一个初步的了解,后面结合代码给大家进一步讲解。
2、区分流的输入与输出
在程序中所有的数据都是以流的方式进行传输的,程序需要数据的时候就用输入流读取数据,当程序需要将计算好的数据进行保存到文件或者输出到其他系统时,就用输出流写出数据。
简单来说的话,就是以我们的程序为中心,如果是外部的数据流向程序,那么就是输入流,输入流一定是读取操作;如果是程序里的数据流出到外部,那么就是输出流,输出流一定是写出操作。
3、IO 操作的套路
Java 中 IO 操作也是有套路的,有标准的操作步骤,主要的操作步骤如下:
1、使用 File 类与文件建立联系
2、选择对应的输入流或者输出流
3、进行读或写操作
4、关闭资源
先对这个套路进行一个了解,后面结合代码一下就明白了,原来套路如此简单。
二、万能钥匙字节流
1、认识字节流
字节流主要操作 byte 类型数据,说它是万能钥匙,是因为它可以处理一切文件,包括文本、word文档、Excel文档、pdf文档、图片、语音、视频等,统统都可以处理。
字节流分为字节输入流和字节输出流,在 Java 中 字节输入流用 InputStream 表示,字节输出流用 OutputStream 表示。
字节输入流:InputStream 是一个抽象类,必须依靠其子类 FileInputStream 来读取文件内容,输入到程序中。我们常用的方法是:
int read(byte b[]) //读取byte数组中的内容,返回读入的长度
close() //关闭资源
字节输出流:OutputStream 是一个抽象类,必须依靠其子类 FileOutputStream 来读取文件内容,输入到程序中。我们常用的方法是:
//将一个制定范围的byte数组输出
void write(byte b[], int off, int len)
close() //关闭资源
flush() // 在关闭资源的时候默认会调用刷新方法
2、字节输出流 FileOutputStream 的使用
我们来看一个例子,把“演示字节输出流的使用\r\n用 FileOutputStream 类操作!”的文本输出到 D:/file/txt/output.txt 文件中。
因为文件操作有可能发生 FileNotFoundException 和 IOException,为了精简代码,便于阅读主要代码,除了本例子以外,后续的例子我会直接使用 throws 关键字抛出异常,并且关闭资源也不放在finally里,这样可以减少 try...catch...finally的代码。
@Test
public void testOutput() {
// 1、建立联系, File对象, 输出文件的地址
// 如果文件不存在则可以创建文件并写入,
// 但是如果加了文件夹,那么文件夹不存在则会产生FileNotFoundException,系统找不到指定的路径
String path = "D:/txt/output.txt";
File file = new File(path);
// 2、选择流
// 由于os要在finally中用到,放到try的外部,以提升os的变量作用范围
OutputStream os = null;
try {
// 用FileOutputStream子类实例化父类OutputStream
// 以追加的方式输出到文件,必须是true,否则就会覆盖原有的文件
os = new FileOutputStream(file, true);
// 3、操作
String info = "演示字节输出流的使用\r\n用 FileOutputStream 类操作!\r\n";
byte[] b = info.getBytes();// 字符串转字节数组
os.write(b, 0, b.length);// 写出
// 要养成这个习惯,为了避免缓存没有写出去,需要显示地flush一下
os.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
System.out.println("文件不存在");
} catch (IOException e) {
e.printStackTrace();
System.out.println("文件写出失败");
} finally {
try {
// 4、释放资源
if (os != null) {
os.close();
}
} catch (Exception e2) {
System.out.println("关闭文件输出流资源失败");
}
}
}
运行结果:
3、字节输入流 FileInputStream 的使用
上面的例子我们学会了字节输出流的使用,下面用字节输入流 FileInputStream 来读取上面的文件内容。
@Test
public void testInput() throws IOException {
// 1、建立联系
File file = new File("D:/output.txt");
// 2、选择流
InputStream is = new FileInputStream(file);
// 3、读操作:即不断地读取
byte[] b = new byte[1024]; // 缓存数组
int len = 0; // 接收实际读取的大小
while ((len = is.read(b)) != -1) {
// 能读取到数据则输出,字节数组转成字符串
String info = new String(b, 0, len);
System.out.println(info);
}
is.close();
}
运行结果:
演示字节输出流的使用
用 FileOutputStream 类操作!
演示字节输出流的使用
用 FileOutputStream 类操作!
4、使用字节流,完成图片文件的拷贝
下面的例子演示如何通过字节流对图片文件进行拷贝操作,假设把 tomcat.png 拷贝成 tomcat1.jpg。
文件的拷贝操作的思路就是,用字节输入流读取图片 tomcat.png 的内容,用字节输出流写出到 tomcat1.jpg 文件中,根据文件操作的套路,很容易就能写出以下的代码:
@Test
public void testCopy() throws IOException {
// 1、使用File类与文件建立联系
File srcFile = new File("D:/file/image/tomcat.png");
File destFile = new File("D:/file/image/tomcat1.jpg");
// 2、选择对应的输入流或者输出流
InputStream is = new FileInputStream(srcFile);
OutputStream os = new FileOutputStream(destFile);
// 3、进行读或写操作
byte[] b = new byte[1024];
int len = 0;
while ((len = is.read(b)) != -1) {
// 判断每次读取的内容长度,如果不等于-1,表示文件没有读完
// 选择带参数的write方法,就是为了避免byte缓存比实际内容多的时候,输出多余的空内容
os.write(b, 0, len);
}
os.flush();
// 4、关闭资源,先创建的后关闭
os.close();
is.close();
}
运行结果:
三、纯文本操作字符流
1、认识字符流
字符流主要操作纯文本类型数据,只能处理 txt、html 等文本类型的数据,在程序中一个字符等于两个字节,Java 提供了 Reader 类和 Writer 类用于专门操作字符流。
字符流也分为字符输入流和字符输出流,在 Java 中 字符输入流用 Reader 表示,输出流用 Writer 表示。
字符输入流:Reader 是一个抽象类,必须依靠其子类 FileReader 来读取纯文本文件内容,输入到程序中。我们常用的方法是:
int read(char cbuf[]) //读取char数组中的内容,返回读入的长度
close() //关闭资源
字符输出流:Writer 是一个抽象类,必须依靠其子类 FileWriter 来读取纯文本文件内容,输入到程序中。我们常用的方法是:
//将一个字符串输出
void write(String str)
//将一个字符数组输出
void write(char cbuf[], int off, int len)
close() //关闭资源
flush() // 在关闭资源的时候默认会调用刷新方法
2、字符输出流 FileWriter 的使用
我们来看一个例子,把“演示字符输出流的使用\r\n用 FileWriter 类操作!”的文本输出到 D:/file/txt/output_char.txt 文件中。
@Test
public void testWriter() throws IOException {
// 1、使用File类与文件建立联系
File file = new File("D:/file/txt/output_char.txt");
// 2、选择对应的输入流或者输出流
Writer writer = new FileWriter(file, true);
String info = "演示字符输出流的使用\r\n用 FileWriter 类操作!\r\n";
// 3、进行写操作
writer.write(info); //将一个字符串组输出
writer.flush();
// 4、关闭资源
writer.close();
}
运行结果:
3、字符输入流 FileReader 的使用
上面的例子我们学会了字符输出流的使用,下面用字符输入流 FileReader 来读取上面的文件内容。
@Test
public void testReader() throws IOException {
// 1、使用File类与文件建立联系
File file = new File("D:/file/txt/output_char.txt");
// 2、选择对应的输入流或者输出流
Reader reader = new FileReader(file);
char[] cbuf = new char[1024];
int len = 0;
// 3、进行写操作
while ((len = reader.read(cbuf)) != -1) {
String info = new String(cbuf, 0, len); // 字符数组转成字符串
System.out.println(info);
}
// 4、关闭资源
reader.close();
}
运行结果:
演示字符输出流的使用
用 FileWriter 类操作!
演示字符输出流的使用
用 FileWriter 类操作!
4、利用字符流,完成 txt文本文件的拷贝
下面的例子演示如何通过字符流对图片文件进行拷贝操作,把 output_char.txt 拷贝成 output_char1.txt。
@Test
public void testTxtCopy() throws IOException {
// 1、使用File类与文件建立联系
File srcFile = new File("D:/file/txt/output_char.txt");
File destFile = new File("D:/file/txt/output_char1.txt");
// 2、选择对应的输入流或者输出流
Reader read = new FileReader(srcFile);
Writer write = new FileWriter(destFile);
// 3、进行读写操作
char[] cbuf = new char[1024];
int len = 0;
while ((len = read.read(cbuf)) != -1) {
write.write(cbuf, 0, len); //将一个字符数组输出
}
write.flush();
// 4、关闭资源
write.close();
read.close();
}
运行结果:
四、字节流与字符流的区别
1、字符输出流在写出文件时用到了缓存区
除去刚才讲过的,字节流可以处理一切文件,字符流只能处理纯文本文件,两者还有一个明显的差异,那就是字符输出流在操作文件时使用了缓冲区,通过缓冲区再写出到文件,而字节输出流直接操作文件。
1、通过源码可以证明字符输出流用到了缓存区
2、通过两段代码的输出结果证明字符输出流用到了缓存区
- 验证字符流:
/**
* 把flush方法和close方法去掉,观察程序运行结果,用字符流输出内容到文件是空的
*/
@Test
public void testWriter1() throws IOException {
// 1、使用File类与文件建立联系
File file = new File("D:/file/txt/output_char_buffer.txt");
// 2、选择对应的输入流或者输出流
Writer writer = new FileWriter(file, true);
String info = "把flush方法和close方法去掉,观察程序运行结果,输出的内容文件是空的!\r\n";
// 3、进行写操作
writer.write(info);
}
运行结果:
- 验证字节流:
/**
* 把flush方法和close方法去掉,观察程序运行结果,用字节流可以输出内容到文件
*/
@Test
public void testOutput1() throws IOException {
// 1、使用File类与文件建立联系
File file = new File("D:/file/txt/output_char_output.txt");
// 2、选择对应的输入流或者输出流
OutputStream os = new FileOutputStream(file, true);
// 3、进行写操作
String info = "把flush方法和close方法去掉,观察程序运行结果,输出的内容文件是空的!\r\n";
byte[] b = info.getBytes();// 字符串转字节数组
os.write(b, 0, b.length);// 写出
}
运行结果:
通过以上的 2 段程序,可以看出,字符流是有缓存的,如果我们没有调用 flush 方法,并且没有调用 close 方法,是无法把内容写到文件中的。但是同样的没有调用 flush 方法和 close 方法,字节流确可以把内容写出到文件。
- 验证字符流调用 flush方法,不调用 close 方法的结果
/**
* 调用flush方法,不调用close方法,观察程序运行结果,用字符流输出内容到文件是可以的,说明字符输出流确实用到了缓冲区
*/
@Test
public void testWriter2() throws IOException {
// 1、使用File类与文件建立联系
File file = new File("D:/file/txt/output_char_writer.txt");
// 2、选择对应的输入流或者输出流
Writer writer = new FileWriter(file);
String info = "调用flush方法,不调用close方法,观察程序运行结果,用字符流输出内容到文件是可以的,说明字符输出流确实用到了缓冲区!\r\n";
// 3、进行写操作
writer.write(info);
// 4、强制刷出
writer.flush();
}
运行结果:
- 验证字符流调用 close 方法,不调用 flush 方法的结果
/**
* 调用close方法,不调用flush方法,观察程序运行结果,用字符流输出内容到文件是可以的,说明字符输出流确实用到了缓冲区
*/
@Test
public void testWriter3() throws IOException {
// 1、使用File类与文件建立联系
File file = new File("D:/file/txt/output_char_writer.txt");
// 2、选择对应的输入流或者输出流
Writer writer = new FileWriter(file);
String info = "调用close方法,不调用flush方法,观察程序运行结果,用字符流输出内容到文件是可以的,说明字符输出流确实用到了缓冲区!\r\n";
// 3、进行写操作
writer.write(info);
// 4、关闭资源
writer.close();
}
运行结果:
通过以上的 2 段程序,可以看出,字符流是有缓存的,通过显示调用 flush 方法可以把缓存内容输出到文件,如果没有调用 flush 方法,在调用 close 方法时,默认也是会把缓存内容输出到文件。
切记字符输出流在flush方法和close方法都没有调用的时候,是无法输出内容到文件的。为了避免出现此类问题,我们在使用输出流的时候,不管是字节流还是字符流最好都显示的调用一下 flush 方法。
讲了这么多,大家觉得我们在操作文件的时候是用字节流好呢还是用字符流好呢,答案是使用字节流更好,因为所有的文件在磁盘中以及网络传输都是以二进制的字节传输的,所以在实际开发中,字节流用的比较广泛。
我们再来明确一下,文件操作的套路只有4步:
1、使用File类与文件建立联系
2、选择对应的输入流或者输出流
3、进行读或写操作
4、关闭资源
另外读写操作也是有固定套路的:
byte[] b = new byte[1024];
int len = 0;
while ((len = is.read(b)) != -1) {
os.write(b, 0, len);
}