整理 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 毫秒

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

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1