【Java I/O 流】4 - 2 字节流的文件输入输出流

§4-2 字节流的文件输入输出流

4-2.1 I/O 流体系

导语:程序是在内存当中运行的,运行中的程序,其数据也存储在内存当中。担当程序终止,在内存中的数据就会丢失。而若将内存中的数据存储到文件中,即使程序终止了,下一次打开程序时,只需要从文件中读取数据,就可以恢复原有的数据。

这就涉及到两个重要的问题,一个是文件的位置,一个是文件的读写。在上一节中,我们介绍了 File 类,它表示文件或目录的路径,可用于获取文件信息(大小、文件名、修改时间等)、判断类型、创建删除文件(夹)等操作,解决了文件位置的问题。但这些操作仅限于对文件本身的操作,并不能读写其中的数据。

I/O 流提供了存储和读取数据的解决方案,可用于读写文件的数据(或网络中的数据)。I/O 流用于程序读取(input)数据、写入(output)数据,文件的读写方向是相对于程序而言的。I/O 流这一名字中,I 表示 Input(输入),O 表示 Output(输出),流意味着像水流一样传输数据。

I/O 流体系十分庞大,但是可以根以下方式分类:

image

其中,纯文本文件的一种验证方式是能否用 Windows 记事本打开,且人类能够正常阅读。是则为纯文本文件,否则则不是。

例如,txt, md, lrc 等文件都是纯文本文件。对于非纯文本文件,则应当使用字节流操作。

按操作文件类型划分,则可以进一步分类为:

image

作为 I/O 流章节的开端,我们先来看看文件的字节流。

字节流主要有两个抽象类 InputStreamOutputStream,二者的子类有很多,但它们的命名都体现了它们的作用和继承结构,如下图所示:

image

4-2.2 FileOutputStream 文件输出流

FileOutputStream 位于 java.io,是 OutputStream 的一个子类,用于操作本地文件的字节输出流,可以把程序中的数据写到本地文件中。

书写步骤

  1. 创建字节输出流对象;

    • 构造器具有多种重载,可传入表示指定文件路径的字符串,也可以是一个对应的 File 对象;

    • 必须保证所指定文件的父级路径存在,否则会抛出异常 FileNotFoundException

    • 若文件不存在(父级路径必须要存在),则会新建该文件;

    • 多种重载的构造底层实际上调用的是 FileOutputStream(File file, boolean append),由最后一个参数决定是否覆盖;

      一般而言,其他重载会传入 false,意味着在对象创建时清空文件;

      可以手动调用该构造方法并传入 true,表示开启续写(追加)而不清空文件;

  2. 写出数据;

    • write 方法用于向文件写出数据,但接收的是字节型或整型数据(byte, int);
    • 这些形参最终会转换成对应 ASCII 字符,并写到文件中;
    • 该方法可多次调用,多次向文件中写出数据;
  3. 释放资源;

    • 调用 close 方法关闭流,解除 Java 程序对文件的占用;
    • 应当在每次使用完流之后释放资源,解除占用;

三个方法都会抛出编译时异常,应当及时处理。

写出数据的方式

方法 描述 说明
void write(int b) 将指定字节写到文件输出流 一次写一个字节数据
void write(byte[] b) 将指定字节数组中的 b.length 个字节写到文件输出流 一次写一个字节数组数据
void write(byte[] b, int off, int len) 将指定字节数组中第 off 个位置起,长度为 len 的字节写到文件输出流 一次写一个字节数组的部分数据
byte[] getBytes() 将字符串转换为字节型数组 String 中的一个成员方法
  • 利用最后一个方法,就可以实现从字符串到字节型数组的转换,从而得到 write 的参数。
  • 若要换行,在字符串中写入对应的换行操作即可,但不同的换行操作因操作系统而异。在 Windows 中为 \r\n(先回车再换行),在 Linux 系统中为 \n,在 macOS 中为 \r。Java 对回车换行做了优化,在 Windows 中只需写其中一个,Java 在底层会自动补全。

4-2.3 FileInputStream 文件输入流

FileInputStream 位于 java.io,是 InputStream 的一个子类,用于操作本地文件的字节输入流,可以把文件中的数据读取到程序中。

书写步骤

  1. 创建字节输入流对象;
    • 构造器具有多种重载,可传入表示指定文件路径的字符串,也可以是一个对应的 File 对象;
    • 若文件不存在,则会抛出异常 FileNotFoundException
  2. 读取文件数据;
    • 调用 read 方法读取文件中一个字节的数据,方法返回一个整型(int)数据;
    • 返回的 int 整型数据实际上是不同字符所对应的 ASCII 编码或已读取字节数;
    • read 方法具有多种重载;
    • 方法可以多次调用,不断读取文件数据,直到无数据可读时(读到文件末尾),返回 -1
  3. 释放资源;
    • 调用 close 方法关闭流,解除 Java 程序对文件的占用;
    • 应当在每次使用完流之后释放资源,解除占用;

三个方法都会抛出编译时异常,应当及时处理。

循环读取文件数据

int b;
while ((b = fis.read()) != -1)
    System.out.print((char) b);

read 方法的实现十分类似于迭代器。一开始,指针指向文件开始处,每调用一次,返回所指数据并移动指针到下一位。若指针来到文件末尾,无数据可读则返回 -1。循环遍历切忌重复调用 read 方法,应当先用第三方变量接收。

读取数据的方式

方法 描述
int read() 从输入流中读取一字节的数据
int read(byte[] b) 从输入流中读取 b.length 长度的数据到数组中
int read(byte[] b, int off, int len) 从输入流中读取 len 长度的数据放到数组 off 偏移量处
  • 方法会尽可能多地读取字节;
  • 无参 read 返回的是所读取的字节内容;
  • 有参方法返回读取字节数,并将数据存入数组中;
  • 存入数组时,将新读取的数据覆盖数组对应位置上原有的数据,若新读取的数据长度小于数组长度,则会导致覆盖不完全;
  • 若已经来到了流的末端,无更多数据可读时,方法返回 -1

4-2.4 文件拷贝

小型文件拷贝

FileInputStream fis = new FileInputStream("JavaSE\\aaa\\a.txt");
FileOutputStream fos = new FileOutputStream("JavaSE\\aaa\\copy.txt");

//循环读取,边读边写
int b;
long time = System.currentTimeMillis();
while ((b = fis.read()) != -1)
    fos.write(b);

//关闭流,先开的最后关
fos.close();
fis.close();

该方法逐字节读写,速度过慢,仅适用于小型文件拷贝。

优化思路:采用 read 的有参重载,使用数组来读写,提高读写效率。

但值得注意的是,虽然说数组越大时间效率越高,但算法的空间复杂度也会上涨。这体现了算法在时间和空间上的矛盾。一般可以考虑采用长度为 1024 的整数倍的数组。

//常规复制:多字节读写,提升速度
File src = new File("JavaSE\\aaa\\a.txt");
File dest = new File("JavaSE\\aaa\\copy.txt");

//创建流
FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest);

long time = System.currentTimeMillis();

//多字节读写
byte[] bytes = new byte[1 << 21];   //2MB
int len;
while ((len = fis.read(bytes)) != -1) {
    fos.write(bytes, 0, len);
}

time = System.currentTimeMillis() - time;
System.out.println("拷贝用时(毫秒):" + time);

//关闭流:先开后关
fos.close();
fis.close();

4-2.5 异常处理

流十分容易触发异常抛出,因此有必要处理可能抛出的异常。

创建、读写、关闭三个操作都会可能抛出异常,所抛出的异常都继承自 IOException

下面以文件拷贝的代码为例,介绍三种异常处理方案。

方案一 - 手动处理异常

//导入语句省略
public static void main(String[] args) {
    //手动处理异常
    //常规复制:多字节读写,提升速度
    File src = new File("JavaSE\\aaa\\a.txt");
    File dest = new File("JavaSE\\aaa\\copy.txt");

    //将变量声明放在语句块外,防止 finally 语句块中的关闭语句因为作用域无法访问
    FileInputStream fis = null;
    FileOutputStream fos = null;

    try {
        //创建流:可能抛出 FileNotFoundException
        fis = new FileInputStream(src);
        fos = new FileOutputStream(dest);

        //多字节读写
        byte[] bytes = new byte[1 << 21];   //2MB
        int len;
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    } finally {
        //释放资源
        //关闭流:先开后关:也可能会抛出 IOException,嵌套环绕处理
        //考虑到若创建对象时就抛出了异常,二者可能为 null,应当先做非空判断,防止抛出空指针异常
        if (fos != null) {
            try {
                fos.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        if (fis != null) {
            try {
                fis.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

应当留意 try-catch-finally 语句块的执行逻辑,使用嵌套 try-catch-finally 解决 finally 语句块中可能抛出的异常。

方案二 - JDK 7 版本的自动释放:自 JDK 7 起,所有实现了 AutoCloseable 接口的实现类对象,在一定情况下可以自动释放资源。

InputStreamOutputStream 实现自 CloseableCloseable 又是 AutoCloseable 的子接口,因此能够实现自动释放资源。

可以自动释放资源的实现类对象,在环绕处理时可以省去 finally 语句块中的 close 语句,进而省略掉整个 finally 语句块。

public static void main(String[] args) {
    //JDK 7 的自动释放资源
    //手动处理异常
    //常规复制:多字节读写,提升速度
    File src = new File("JavaSE\\aaa\\a.txt");
    File dest = new File("JavaSE\\aaa\\copy.txt");

    //在 try 语句块后的括号内只能写实现了 AutoCloseable 类的对象的创建语句
    //创建流:可能抛出 FileNotFoundException
    try (FileInputStream fis = new FileInputStream(src);
         FileOutputStream fos = new FileOutputStream(dest)) {
        //多字节读写
        byte[] bytes = new byte[1 << 21];   //2MB
        int len;
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

括号内只能写创建语句,且必须是实现了 AutoCloseable 的类的对象的创建语句。

多个语句间用 ; 分隔,最后一句的分号可省略。

方案二 - JDK 9 版本的自动释放:JDK 7 版本的自动释放会使得 try 后的代码阅读性较差。JDK 9 允许将创建对象的语句挪至外部,括号内仅需要写上对应的对象即可。

public static void main(String[] args) throws FileNotFoundException {
    //JDK 9 的自动释放
    //手动处理异常
    //常规复制:多字节读写,提升速度
    File src = new File("JavaSE\\aaa\\a.txt");
    File dest = new File("JavaSE\\aaa\\copy.txt");

    //创建流:可能抛出 FileNotFoundException
    FileInputStream fis = new FileInputStream(src);
    FileOutputStream fos = new FileOutputStream(dest);

    //JDK 9 后,在 try 语句块后的括号内可以只写上已创建好的 AutoCloseable 实现类对象
    try (fis; fos) {
        //多字节读写
        byte[] bytes = new byte[1 << 21];   //2MB
        int len;
        while ((len = fis.read(bytes)) != -1) {
            fos.write(bytes, 0, len);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

try 小括号内不同的变量仍用 ; 分隔。构造器仍可能会抛出 FileNotFoundException,但直接往外部抛出。在实际的开发过程中,遇到异常基本上做抛出处理。

posted @ 2023-08-17 21:28  Zebt  阅读(80)  评论(0)    收藏  举报