Java I/O 原理分析
IO 是什么
- 作用:和外界做数据交互。
- I/O是什么:输入,输出。
- 输入:从程序外部读数据到程序内部。
- 输出:从程序内部写数据到程序外部。
- 程序内部:内存。比如 String string = “xxx”,string 就是程序内部。
- 程序外部:程序之外的东西。一般来说就是本地文件和网络;还有就是程序跟外部程序交互,外部程序也是“我的程序“的外部
- 从哪往哪输出:程序内部写数据到程序外部。打个比方,“我” 跟 “书本”,“我” 就是程序内部,“书本” 是程序外部,“我” 要从 “大脑”(内存)里面把 “一句话”(数据)写(write)到 “书本” 上。对于“我”来说就是输出。
- 从哪往哪输入:程序内部从程序外部读数据。跟上面一样,“我”(内部)要从 “书本”(外部)上读(read)“一句话”(数据)到“大脑”(内存)中。对于 “我” 来说就是输入。
IO 怎么用?
插管1
了解了是什么,接下来看怎么用。很简单,如图,就是插管子,也就是用流,对流进行操作。比如:往文件(外部)上插一根输出管 new FileOutputStream("文件路径") ,然后 “内部” 往管子上写数据。
try {
FileOutputStream outputStream = new FileOutputStream("./new.txt")
outputStream.write('a');
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
上面代码就是一个最简单的输出操作,当然这是不完整。大家都知道,文件打开了就必须要关闭,那么什么是文件打开,什么是文件关闭,为什么要关闭呢?
- 文件的打开:在内存里面腾出来一块专门的地方用来保存文件的相关信息(什么文件,存在哪,多大,读到哪一行。用来方便读写),把这些信息放到内存里面就是文件的打开。放到内存里面就会占内存,占系统资源。
- 文件的关闭:读写完文件之后要及时的把文件信息给释放,把内存给释放出来。这个释放的过程就是文件的关闭。具体来说就是各种参数,各种引用什么的全部扔掉,该扔的扔该销毁的销毁,把内存腾出来,把资源腾出来。
那么为什么要关闭就明显了,然后把关闭给加上就是下面这样了:
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream("./new.txt");
outputStream.write('a');
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
为什么要写在 finally 而不写在 try 里面呢?因为如果代码有问题就有可能执行不到 close() 了。
但这样好麻烦啊,而且都是固定代码,又不能减少。别着急,Java7 引入了新方式,在 try 里面就可以直接做回收。如下:
try (FileOutputStream outputStream = new FileOutputStream("./new.txt")) {
outputStream.write('a');
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
当然,能写在 try() 里面的有个限制,必须是实现了 Closeable 接口的类。
能插输出的管子来写,肯定也能插输入的管子来读:
try (FileInputStream inputStream = new FileInputStream("./new.txt")) {
System.out.print((char) inputStream.read());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
接下来介绍插多根管子的情况,看图:
插管2
输出,写的操作
try (FileOutputStream outputStream = new FileOutputStream("./new.txt");
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
BufferedWriter bufferedWriter = new BufferedWriter(writer)) {
bufferedWriter.write("x");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
输入,读的操作
try (FileInputStream inputStream = new FileInputStream("./new.txt");
InputStreamReader reader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(reader)) {
System.out.println(bufferedReader.readLine());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
可以看到这就是一个管道插在另一个管道上,最后插在文件上,进行文件的读写操作。代码中用到了缓冲(buffer),在 BufferedReader 和 BufferedWriter 源码中都会有定义这个缓冲的大小:
...
private static int defaultCharBufferSize = 8192;
...
- 那么 buffer 又是做什么用的呢?
就跟字面意思一样 “缓冲”。
比如:程序内部要从程序外部读数据,就会跟 buffer 说:“我要读 1 个数据。”接着 buffer 就会跟后面的 “管子” 讲:“我要 8192 个数据。” 最后,在缓冲中就会有8192个数据(假设文件中数据足够多),只把1个数据传给了程序内部。这样的话,当下次程序内部要再次读数据的时候,就会直接在 buffer 中读。 - 为什么 buffer 要这么设计?
为了成本,为了效率。因为每次读写文件、网络数据都会非常的耗时间耗性能。
就上面的代码而言,BufferedReader 用到了缓冲,当然 BufferedWriter 也用了缓冲,只有当 buffer 中的数据大小达到 8192 个的时候才会往文件中写。
- 为什么我明明只写了一个 "x" 文件中也能看到数据呢?
首先要说一下 flush() 这个方法。
假如现在有这样一个需求,要求每次往 buffer 中写的数据不管大小,都要一股脑的写到文件上。那我如果要写的数据达不到 8192 个的时候就写不到文件里面了。
这个时候就要用到 flush 了,flush 的意思是:冲马桶,冲厕所。“唰”的一下全部冲过去了。
但是上面代码中并没用到 flush,是因为当文件关闭的时候会有自动的 flush 行为。
那如果是自动的话,是不是说每次都可以不写 flush 呢?也不是,接下来介绍一种要用到 flush 的情况:
// 模拟一个服务器,读到什么内容就写什么内容返回
try {
ServerSocket serverSocket = new ServerSocket(8080);
// 等待别人的请求,阻塞式的
Socket socket = serverSocket.accept();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String data;
while (true) {
data = reader.readLine();
writer.write(data);
// 冲马桶行为。这个时候就不能等关闭的时候自动冲了。
writer.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
最后说一下文件的复制,怎么做文件复制呢?原理就是一个字节一个字节的搬。从一个文件读数据,写到另一个文件去。
try (FileOutputStream outputStream = new FileOutputStream("./new_copy.txt");
FileInputStream inputStream = new FileInputStream("./new.txt")) {
byte[] bytes = new byte[1024];
int size;
// 每次记录读取到的数据 size
while ((size = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, size);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
这里需要考虑一个情况,当读到最后一次,剩余数据的大小不足1024的时候,bytes 中会有残留的上一次的数据。比如:new.txt 中一共有 1025 个,第一次读了1024,第二次读的时候本应该只有一个,但是除了 0 位置上的数据变了之外,后面所有的数据还是上一次读到的数据。所以要记录读到的大小。
总结:
本文介绍了 Java I/O 是什么,怎么用,原理就是插管子(通过流进行对文件或网络的读写操作)。也可以往管子上再插管子。理解了这些,我相信以后再也不要在用到 I/O 的时候到网上复制粘贴了。
(ps:原本来准备把 NIO原理,Okio使用也做个总结,想不到,想不到。。。输出比输入难啊 - -)