Java中操作文件的方式本质上只有两种:字符流和字节流,而字节流和字符流的实现类很多,所以我们在写文件的时候可以选择各种类。 完成。 在这篇文章中,我们将对这些方法进行盘点,顺便测试一下它们的性能,从而选择出最适合我们的写法。
在正式开始之前,先了解几个基本概念:流、字节流、字符流的定义和区别。
0.什么是流?
Java中的“流”是一个抽象概念和隐喻,就像水流一样,水流从一端流向另一端,而Java中的“水流”是数据,数据会从一端“流”到另一端。
根据流的方向性,我们可以将流分为输入流和输出流。 当程序需要从数据源中读取数据时,它会打开一个输入流。 相反,将数据写入数据源目的地。 有时会打开一个输出流,数据源可以是文件、内存或网络。
1、什么是字节流?
字节流的基本单位是字节(Byte),一个字节通常为8位,用于处理二进制(数据)。 字节流有两个基类:InputStream(输入字节流)和OutputStream(输出字节流)。
常用字节流的继承关系图如下图所示:
其中InputStream用于读操作,OutputStream用于写操作。
2.什么是字符流?
字符流的基本单位是Unicode,大小为两个字节(Byte),通常用于处理文本数据。 字符流的两个基类:Reader(输入字符流)和Writer(输出字符流)。
常用字符流的继承关系图如下图所示:
3.流分类
流可以按照不同的维度进行分类,比如流的方向java追加写入txt文件,传输的单位java追加写入txt文件,或者流的功能,比如下面的。
①按流向分类②按传输数据单元分类③按功能分类
PS:我们通常以传输数据为单位对流进行分类。
4. 6种写文件方式
写入文件的方法主要派生自字符流Writer和输出字节流OutputStream的子类,如下图所示:
上面标有✅的类就是用来实现写文件的类。 另外,在JDK 1.7中还提供了Files类来实现对文件的各种操作。 接下来,我们将分别来看它们。
方法一:FileWriter
FileWriter是“字符流”系统的一员,也是文件写入的基础类。 它包含 5 个构造函数,可以传递特定的文件位置或 File 对象。 第二个参数表示是否追加文件,默认值False表示重写文件内容,而不是追加文件内容(关于如何追加文件,后面会讲到)。
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中文社群.");
}
然后我们打开写入的文件,结果如下:
关于资源释放:在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);
}
}
}
在查看结果之前,我们先去对应的文件夹查看写入的文件是否正常,如下图:
从上面的结果可以看出,每个方法正常写入26MB的数据,它们最终的执行结果如下图所示:
从上面的结果可以看出,字符流的运行速度是最快的。 这是因为我们这次测试的代码是对字符串进行操作的,所以在使用字节流的时候,需要先将字符串转换成字节流,这样在执行效率上没有优势。
从上面的结果可以看出,性能最好的是带缓冲区的字符串写入流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的?
每日打卡赢积分兑换书籍入口