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使用也做个总结,想不到,想不到。。。输出比输入难啊 - -)

发布了3 篇原创文章 · 获赞 9 · 访问量 1709

猜你喜欢

转载自blog.csdn.net/MaBiAHaiWang/article/details/103838431