Java中操作文件的方式本质上只有两种:字符流和字节流,而字节流和字符流的实现类很多,所以我们在写文件的时候可以选择各种类。 完成。 在这篇文章中,我们将对这些方法进行盘点,顺便测试一下它们的性能,从而选择出最适合我们的写法。

在正式开始之前,先了解几个基本概念:流、字节流、字符流的定义和区别。

0.什么是流?

Java中的“流”是一个抽象概念和隐喻,就像水流一样,水流从一端流向另一端,而Java中的“水流”是数据,数据会从一端“流”到另一端。

根据流的方向性,我们可以将流分为输入流和输出流。 当程序需要从数据源中读取数据时,它会打开一个输入流。 相反,将数据写入数据源目的地。 有时会打开一个输出流,数据源可以是文件、内存或网络。

1、什么是字节流?

字节流的基本单位是字节(Byte),一个字节通常为8位,用于处理二进制(数据)。 字节流有两个基类:InputStream(输入字节流)和OutputStream(输出字节流)。

常用字节流的继承关系图如下图所示:

java追加写入txt文件_java多线程写入txt文件_java 循环写入txt文件

其中InputStream用于读操作,OutputStream用于写操作。

2.什么是字符流?

字符流的基本单位是Unicode,大小为两个字节(Byte),通常用于处理文本数据。 字符流的两个基类:Reader(输入字符流)和Writer(输出字符流)。

常用字符流的继承关系图如下图所示:

java 循环写入txt文件_java多线程写入txt文件_java追加写入txt文件

3.流分类

流可以按照不同的维度进行分类,比如流的方向java追加写入txt文件,传输的单位java追加写入txt文件,或者流的功能,比如下面的。

①按流向分类②按传输数据单元分类③按功能分类

PS:我们通常以传输数据为单位对流进行分类。

4. 6种写文件方式

写入文件的方法主要派生自字符流Writer和输出字节流OutputStream的子类,如下图所示:

java多线程写入txt文件_java追加写入txt文件_java 循环写入txt文件

上面标有✅的类就是用来实现写文件的类。 另外,在JDK 1.7中还提供了Files类来实现对文件的各种操作。 接下来,我们将分别来看它们。

方法一:FileWriter

FileWriter是“字符流”系统的一员,也是文件写入的基础类。 它包含 5 个构造函数,可以传递特定的文件位置或 File 对象。 第二个参数表示是否追加文件,默认值False表示重写文件内容,而不是追加文件内容(关于如何追加文件,后面会讲到)。

java 循环写入txt文件_java多线程写入txt文件_java追加写入txt文件

FileWriter类实现如下:

/**
  * 方法 1:使用 FileWriter 写文件
  * @param filepath 文件目录
  * @param content  待写入内容
  * @throws IOException
  */

public static void fileWriterMethod(String filepath, String content) throws IOException {
    try (FileWriter fileWriter = new FileWriter(filepath)) {
        fileWriter.append(content);
    }
}

你只需要传入具体的文件路径和要写入的内容即可。 调用代码如下:

public static void main(String[] args) {
    fileWriterMethod("/Users/mac/Downloads/io_test/write1.txt""哈喽,Java中文社群.");
}

然后我们打开写入的文件,结果如下:

java 循环写入txt文件_java多线程写入txt文件_java追加写入txt文件

关于资源释放:在JDK 7以上的版本中,我们只需要使用try-with-resource来释放资源,比如使用try (FileWriter fileWriter = new FileWriter(filepath)) {…} 自动释放FileWriter资源即可得以实现。

方法二:BufferedWriter

BufferedWriter 也是字符流系统的一员。 BufferedWriter与FileWriter不同,BufferedWriter有自己的buffer,因此在写入文件时具有更高的性能(下面将对两者进行测试)。

小知识点:缓冲区

缓冲区,也称为高速缓存,是内存空间的一部分。 也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间称为缓冲区。

缓冲区的优点以文件流的写入为例。 如果我们不使用buffer,每次写操作时CPU都会和低速存储设备,也就是磁盘进行交互,整个文件的写入速度都会受到低速存储设备的限制(磁盘)。 但如果使用缓冲区,每次写操作都会先将数据保存在高速缓冲存储器中,当缓冲区中的数据达到一定阈值时,一次性将文件写入磁盘。 因为内存的写入速度比磁盘的写入速度要快很多,所以当有缓冲区的时候,文件的写入速度会大大提高。

了解了缓存区的优势之后,让我们回到本文的主题。 接下来,我们使用 BufferedWriter 来写入文件。 实现代码如下:

/**
 * 方法 2:使用 BufferedWriter 写文件
 * @param filepath 文件目录
 * @param content  待写入内容
 * @throws IOException
 */

public static void bufferedWriterMethod(String filepath, String content) throws IOException {
    try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filepath))) {
        bufferedWriter.write(content);
    }
}

调用代码与方法一类似,这里不再赘述。

方法三:PrintWriter

PrintWriter 也是字符流系统的一员。 虽然叫“字符打印流”,但也可以用来写文件。 实现代码如下:

/**
 * 方法 3:使用 PrintWriter 写文件
 * @param filepath 文件目录
 * @param content  待写入内容
 * @throws IOException
 */

public static void printWriterMethod(String filepath, String content) throws IOException {
    try (PrintWriter printWriter = new PrintWriter(new FileWriter(filepath))) {
        printWriter.print(content);
    }
}

从上面的代码可以看出,PrintWriter和BufferedWriter都必须基于FileWriter类来调用。

方法四:FileOutputStream

以上三个例子是关于字符流写入文件的一些操作,接下来我们将使用字节流来完成文件的写入。 我们会先使用String自带的getBytes()方法将字符串转换成二进制文件,然后写入文件。 其实现代码如下:

/**
 * 方法 4:使用 FileOutputStream 写文件
 * @param filepath 文件目录
 * @param content  待写入内容
 * @throws IOException
 */

public static void fileOutputStreamMethod(String filepath, String content) throws IOException {
    try (FileOutputStream fileOutputStream = new FileOutputStream(filepath)) {
        byte[] bytes = content.getBytes();
        fileOutputStream.write(bytes);
    }
}

方法五:BufferedOutputStream

BufferedOutputStream 是字节流系统的一员。 与FileOutputStream不同的是它有缓冲功能,所以性能更好。 其实现代码如下:

/**
 * 方法 5:使用 BufferedOutputStream 写文件
 * @param filepath 文件目录
 * @param content  待写入内容
 * @throws IOException
 */

public static void bufferedOutputStreamMethod(String filepath, String content) throws IOException {
    try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(
            new FileOutputStream(filepath))) {
        bufferedOutputStream.write(content.getBytes());
    }
}

方法 6:文件

接下来的操作方法和前面的代码不同。 接下来,我们将使用JDK 7中提供的一个新的文件操作类Files来实现文件写入。

Files类是JDK 7新增的文件操作类,它提供了大量处理文件的方法,如文件复制、读取、写入、获取文件属性、快速遍历文件目录等。 这些方法极其方便了对文件的操作,其实现代码如下:

/**
 * 方法 6:使用 Files 写文件
 * @param filepath 文件目录
 * @param content  待写入内容
 * @throws IOException
 */

public static void filesTest(String filepath, String content) throws IOException {
    Files.write(Paths.get(filepath), content.getBytes());
}

以上几种方式都可以实现文件写入,那么哪种方式性能更高呢? 接下来我们测试一下。

5.性能测试

我们先建一个比较大的字符串,然后用上面六种方法测试文件写入速度,最后打印结果。 测试代码如下:

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;

public class WriteExample {
    public static void main(String[] args) throws IOException {
        // 构建写入内容
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 1000000; i++) {
            stringBuilder.append("ABCDEFGHIGKLMNOPQRSEUVWXYZ");
        }
        // 写入内容
        final String content = stringBuilder.toString();
        // 存放文件的目录
        final String filepath1 = "/Users/mac/Downloads/io_test/write1.txt";
        final String filepath2 = "/Users/mac/Downloads/io_test/write2.txt";
        final String filepath3 = "/Users/mac/Downloads/io_test/write3.txt";
        final String filepath4 = "/Users/mac/Downloads/io_test/write4.txt";
        final String filepath5 = "/Users/mac/Downloads/io_test/write5.txt";
        final String filepath6 = "/Users/mac/Downloads/io_test/write6.txt";

        // 方法一:使用 FileWriter 写文件
        long stime1 = System.currentTimeMillis();
        fileWriterTest(filepath1, content);
        long etime1 = System.currentTimeMillis();
        System.out.println("FileWriter 写入用时:" + (etime1 - stime1));

        // 方法二:使用 BufferedWriter 写文件
        long stime2 = System.currentTimeMillis();
        bufferedWriterTest(filepath2, content);
        long etime2 = System.currentTimeMillis();
        System.out.println("BufferedWriter 写入用时:" + (etime2 - stime2));

        // 方法三:使用 PrintWriter 写文件
        long stime3 = System.currentTimeMillis();
        printWriterTest(filepath3, content);
        long etime3 = System.currentTimeMillis();
        System.out.println("PrintWriterTest 写入用时:" + (etime3 - stime3));

        // 方法四:使用 FileOutputStream  写文件
        long stime4 = System.currentTimeMillis();
        fileOutputStreamTest(filepath4, content);
        long etime4 = System.currentTimeMillis();
        System.out.println("FileOutputStream 写入用时:" + (etime4 - stime4));

        // 方法五:使用 BufferedOutputStream 写文件
        long stime5 = System.currentTimeMillis();
        bufferedOutputStreamTest(filepath5, content);
        long etime5 = System.currentTimeMillis();
        System.out.println("BufferedOutputStream 写入用时:" + (etime5 - stime5));

        // 方法六:使用 Files 写文件
        long stime6 = System.currentTimeMillis();
        filesTest(filepath6, content);
        long etime6 = System.currentTimeMillis();
        System.out.println("Files 写入用时:" + (etime6 - stime6));

    }

    /**
     * 方法六:使用 Files 写文件
     * @param filepath 文件目录
     * @param content  待写入内容
     * @throws IOException
     */

    private static void filesTest(String filepath, String content) throws IOException {
        Files.write(Paths.get(filepath), content.getBytes());
    }

    /**
     * 方法五:使用 BufferedOutputStream 写文件
     * @param filepath 文件目录
     * @param content  待写入内容
     * @throws IOException
     */

    private static void bufferedOutputStreamTest(String filepath, String content) throws IOException {
        try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(
                new FileOutputStream(filepath))) {
            bufferedOutputStream.write(content.getBytes());
        }
    }

    /**
     * 方法四:使用 FileOutputStream  写文件
     * @param filepath 文件目录
     * @param content  待写入内容
     * @throws IOException
     */

    private static void fileOutputStreamTest(String filepath, String content) throws IOException {
        try (FileOutputStream fileOutputStream = new FileOutputStream(filepath)) {
            byte[] bytes = content.getBytes();
            fileOutputStream.write(bytes);
        }
    }

    /**
     * 方法三:使用 PrintWriter 写文件
     * @param filepath 文件目录
     * @param content  待写入内容
     * @throws IOException
     */

    private static void printWriterTest(String filepath, String content) throws IOException {
        try (PrintWriter printWriter = new PrintWriter(new FileWriter(filepath))) {
            printWriter.print(content);
        }
    }

    /**
     * 方法二:使用 BufferedWriter 写文件
     * @param filepath 文件目录
     * @param content  待写入内容
     * @throws IOException
     */

    private static void bufferedWriterTest(String filepath, String content) throws IOException {
        try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filepath))) {
            bufferedWriter.write(content);
        }
    }

    /**
     * 方法一:使用 FileWriter 写文件
     * @param filepath 文件目录
     * @param content  待写入内容
     * @throws IOException
     */

    private static void fileWriterTest(String filepath, String content) throws IOException {
        try (FileWriter fileWriter = new FileWriter(filepath)) {
            fileWriter.append(content);
        }
    }
}

在查看结果之前,我们先去对应的文件夹查看写入的文件是否正常,如下图:

java追加写入txt文件_java 循环写入txt文件_java多线程写入txt文件

从上面的结果可以看出,每个方法正常写入26MB的数据,它们最终的执行结果如下图所示:

java 循环写入txt文件_java多线程写入txt文件_java追加写入txt文件

从上面的结果可以看出,字符流的运行速度是最快的。 这是因为我们这次测试的代码是对字符串进行操作的,所以在使用字节流的时候,需要先将字符串转换成字节流,这样在执行效率上没有优势。

从上面的结果可以看出,性能最好的是带缓冲区的字符串写入流BufferedWriter,性能最慢的是Files。

PS:以上测试结果仅对字符串操作场景有效。 如果是操作二进制文件,应该使用缓冲字节流BufferedOutputStream。

6.拓展知识:内容加成

上面的代码将重写文件。 如果只想在原来的基础上追加内容,需要在创建写流时设置一个append参数为true。 比如我们使用FileWriter来实现文件追加,实现代码是这样的:

public static void fileWriterMethod(String filepath, String content) throws IOException {
    // 第二个 append 的参数传递一个 true = 追加文件的意思
    try (FileWriter fileWriter = new FileWriter(filepath, true)) {
        fileWriter.append(content);
    }
}

如果你使用的是 BufferedWriter 或 PrintWriter,你还需要在新建 FileWriter 类时将附加参数 append 设置为 true。 实现代码如下:

try (BufferedWriter bufferedWriter = new BufferedWriter(
    new FileWriter(filepath, true))) {
    bufferedWriter.write(content);
}

相比较而言,Files类更为特殊,以实现对文件的额外写入。 调用write方法时需要额外传递一个参数StandardOpenOption.APPEND。 其实现代码如下:

Files.write(Paths.get(filepath), content.getBytes(), StandardOpenOption.APPEND);

七、总结

在本文中,我们展示了6种写文件的方法,分为3类:字符流写法、字节流写法和Files类写法。 操作最方便的是Files类,但性能不是很好。 如果对性能有要求,建议使用带有缓冲区的流来完成操作,比如BufferedWriter或者BufferedOutputStream。 如果写入的内容是字符串,建议使用BufferedWriter,如果写入的内容是二进制文件,建议使用BufferedOutputStream。

参考与致谢

-结尾-

  推荐阅读:

四种最令人讨厌的编程语言:Java、Javascript、C++和Perl

漫话:为什么Java中的main方法必须是public static void的?

10月份Github上最热门的Java开源项目

每日打卡赢积分兑换书籍入口