网上关于Java I/O的文章一抓一大把,可我决定还是自己搜集整理一下,原因有两个:第一,整理一遍可以加深印象;二来,他们的写作不太符合我的阅读习惯,写一篇权当备份了。
归根结底,我们在进行一些数据操作的时候,无非就是对“数据源”和“接收端”的一系列操作,有一个数据源提供数据让我们对数据进行读取,同时也有一个接收端可以将我们读取的数据源转换成文件,而连接数据源和接收端的这个玩意儿就叫做“流”。
按照流操作的数据,我们将流分为
字节流(InputStream和OutputStream及其一系列子类)
字符流(Reader和Writer及其一系列子类)
按照流的流向,我们将流分为
输入流(InputStream和Reader及其一系列子类)
输出流(OutputStream和Writer及其一系列子类)
Java的有一个庞大的IO体系,看下图
它们都存在于java.io包中。
看了上面这一大堆是不是有些懵逼?我也是整理的时候才发现,原来IO家族这么庞大,不过常用的也就那么几个,另外我们从类名也可以看出来该类究竟是输入流还是输出流、究竟是字符流还是字节流,简单又方便。
现在开始由浅入深的把一些日常开发常用到的流进行一下讲解~
有下面几个概念需要明白一下:
流末尾
不管我们使用那个read方法,当读到“文件”的末尾的时候,都会返回一个int型的数据:-1,所以我们在read的时候用这个数据来判断我们是否读到了末尾。
刷新
好多人都喜欢用冲马桶来解释这个舒刷新(简直恶心!鄙视你们!虽然你们没错...)。
因为IO本身就是对硬盘或者是网络的读写,处理速度肯定会比CPU要慢,所以常常需要用到缓存来作为程序和目的地之间的桥梁来暂存数据。
假如我们程序在运行中突然崩溃,那么缓存区中的数据不就丢失了么,幸运的是有flush方法可以帮我们把缓存区中暂存的内容写入到目的地中。调用输出流的close方法会强行flush,但还是建议在程序中手动flush一下。
需要注意的是,并不是所有的输出流都有缓存区,也就是说,并不是所有的输出流都需要flush。
OutputStream类的flush方法并不是abstract的,所以说它的子类如果重写了其flush方法另说,如果没有重写,那么就不需要flush了,ByteArrayOutputStream就不需要~
Writer的flush则是abstract,它的子类都需要重写它的flush方法,那么也可以简单粗暴的理解成字符输出流均需要flush(我没去看源码,不过如果不需要,也没必要设计成abstract了嘛)
关闭流
在我们读写完成之后,需要调用close方法来关闭流。为什么要关闭流呢?一方面关闭流可以强制flush缓存区内的数据,另一方面,系统gc会清理内存,但IO往往还要消耗一定的系统资源,譬如硬盘读写所占用的资源,gc是不会自动回收这方面的资源的。
那么是先关闭输入流还是先关闭输出流呢?这个没有明确规定,只要保证流内数据已经flush就好了。
需要说明的是不管是任何操作,无论是对磁盘中的文件进行读写,还是数据在网络中传输, 最小的存储单元都是字节,而不是字符,但在我们实际开发中却有相当大比例的数据是以字符形式存在的,为了方便操作,就有了字符流(上帝说要有光,就有了光...逃...)。
Reader和Writer是所有字符流的基类,它们提供了一系列方法来对数据进行读写,它们是abstract的,所以只能依靠不同的子类来进行不同类型的操作,它们有一系列的方法来对文件进行读、写、标记、跳过等等。
读:
read() 读取单个字符
read(char[] cbuf) 将字符读入数组。
read(char[] cbuf, int off, int len) 将字符读入数组的某一部分。
写:
write(char[] cbuf) 写入字符数组。
write(char[] cbuf, int off, int len) 写入字符数组的某一部分。
write(int c) 写入单个字符。
write(String str) 写入字符串。
write(String str, int off, int len) 写入字符串的某一部分。
下面是一个入门的例子来演示字符流的读写,我们将D盘中的一个文本文件复制到E盘中去
/**
* 实现将一个文本文件从D盘复制到E盘
* @author
*
*/
public class CopyText {
private static File file = new File("D:\\text.txt");
private static File newFile = new File("E:\\new text.txt");
private FileReader reader;
private FileWriter writer;
public static void main(String[] args) {
new CopyText().copyFile(file, newFile);
}
public void copyFile(File file,File newFile){
try {
reader = new FileReader(file);
writer = new FileWriter(newFile);
int i = 0;
while ((i = reader.read()) != -1) {
writer.write(i);
writer.flush();
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO exception ,code io-e-1000");
}finally{
if(reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO exception ,code io-e-2000");
}
}
if(writer != null){
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO exception ,code io-e-3000");
}
}
}
}
}
上面代码看着貌似有50多行,其实去掉finally中关闭流以及抛出的异常,核心代码也就十来行。
一定要记得关闭流
字节流和字符流大同小异,依旧一个简单的例子
/**
* 复制一个视频
* @author
*
*/
public class CopyVideo {
private static FileInputStream fis;
private static FileOutputStream fos;
public static void main(String[] args) {
try {
File file = new File("D:\\ABDEADEP-486.mp4");
File newFile = new File("E:\\ABDEADEP-486-copy.mp4");
fis = new FileInputStream(file);
fos = new FileOutputStream(newFile);
int i = 0;
while ((i = fis.read()) != -1) {
fos.write(i);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
可以看到,这个例子中并没有用到flush,这也就是我们上面说的原因。
我们来复制一个稍微大一些的文件
public class CopyFileWithArray {
private static File file = new File("D:\\ABDEADEP-486.mp4");
private static File newFile = new File("E:\\new File.mp4");
private static FileInputStream fis;
private static FileOutputStream fos;
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
try {
fis = new FileInputStream(file);
fos = new FileOutputStream(newFile);
int i = 0;
while ((i = fis.read()) != -1) {
fos.write(i);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.println("复制大小为 " + file.length() + " 个字节的文件,共耗时: "
+ (endTime - startTime) + " 毫秒");
}
}
运行之后,结果如下:
复制大小为 64597702 个字节的文件,共耗时: 594721 毫秒
都快10分钟了!!这怎么行,才60多M就耗时这么长时间,显然是不行的。
我们可以看到我们的输入输出流中还提供了一些参数为数组的read、write方法,他们是先将读取到的内容存储到一个字节(或字符)数组中,然后带数组满了之后再写出去,这样就可以大大提升我们的效率。来试一下:
public class CopyFileWithArray {
private static File file = new File("D:\\ABDEADEP-486.mp4");
private static File newFile = new File("E:\\new File.mp4");
private static FileInputStream fis;
private static FileOutputStream fos;
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
try {
fis = new FileInputStream(file);
fos = new FileOutputStream(newFile);
int length = 0;
byte[] arr = new byte[1024]; //设置一个长度为1024的数组,用作缓存区
while ((length = fis.read(arr)) != -1) {
fos.write(arr, 0, length);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
System.out.println("复制大小为 " + file.length() + " 个字节的文件,共耗时: "
+ (endTime - startTime) + " 毫秒");
}
}
运行结果
复制大小为 64597702 个字节的文件,共耗时: 1067 毫秒
效果是不是很明显呢?当然,还有更好的方法,接下来详细说...