整理 Java I/O (一):初识I/O

概述

网上关于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 毫秒

    效果是不是很明显呢?当然,还有更好的方法,接下来详细说...