美文网首页
Java I/O 原理分析

Java I/O 原理分析

作者: z白依 | 来源:发表于2020-01-05 19:37 被阅读0次

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

    相关文章

      网友评论

          本文标题:Java I/O 原理分析

          本文链接:https://www.haomeiwen.com/subject/plecactx.html