别乱用了!文件写入的 6 种方法

百家 作者:程序员的那些事 2022-10-20 13:03:15

在 Java 中操作文件的方法本质上只有两种:字符流和字节流,而字节流和字符流的实现类又有很多,因此在文件写入时我们就可以选择各种各样的类来实现。我们本文就来盘点一下这些方法,顺便测试一下它们性能,以便为我们选出最优的写入方法。

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

0.什么是流?

Java 中的“流”是一种抽象的概念,也是一种比喻,就好比水流一样,水流是从一端流向另一端的,而在 Java 中的“水流”就是数据,数据会从一端“流向”另一端。

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

1.什么是字节流?

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

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

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

2.什么是字符流?

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

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

3.流的分类

流可以根据不同的维度进行分类,比如可以根据流的方向进行分类,也可以根据传输的单位进行分类,还可以根据流的功能进行分类,比如以下几个。

① 按流向分类

  • 输出流:OutputStream?和?Writer?为基类。

  • 输入流:InputStream?和?Reader?为基类。

② 根据传输数据单位分类

  • 字节流:OutputStream?和?InputStream?为基类。

  • 字符流:Writer?和?Reader?为基类。

③ 根据功能分类

  • 字节流:可以从或向一个特定的地方(节点)读写数据。

  • 处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。

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

4.写文件的6种方法

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

以上标注?号的类就是用来实现文件写入的类,除此之外,在 JDK 1.7 中还提供了?Files?类用来实现对文件的各种操作,接下来我们分别来看。

方法 1: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 资源的自动释放。

方法 2:BufferedWriter

BufferedWriter?也属于字符流体系的一员,与?FileWriter?不同的是?BufferedWriter?自带缓冲区,因此它写入文件的性能更高(下文会对二者进行测试)。

小知识点:缓冲区

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

缓冲区的优势以文件流的写入为例,如果我们不使用缓冲区,那么每次写操作 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);??
????}??
}??

调用代码和方法 1 类似,这里就不再赘述了。

方法 3: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?类来完成调用。

方法 4:FileOutputStream

上面 3 个示例是关于字符流写入文件的一些操作,而接下来我们将使用字节流来完成文件写入。我们将使用?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);??
????}??
}??

方法 5: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:Files

接下来的操作方法和之前的代码都不同,接下来咱们就使用 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.性能测试

我们先来构建一个比较大的字符串,然后分别用以上 6 种方法来测试文件写入的速度,最后再把结果打印出来,测试代码如下:

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);??
????????}??
????}??
}??

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

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

从以上结果可以看出,字符流的操作速度最快,这是因为我们本次测试的代码操作的是字符串,所以在使用字节流时,需要先将字符串转换为字节流,因此在执行效率上不占优势。

从上述结果可以看出,**性能最好的是带有缓冲区的字符串写入流?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,也是需要在构建?new 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);??

7.总结

本文我们展示了 6 种写入文件的方法,这 6 种方法总共分为 3 类:字符流写入、字节流写入和?Files?类写入。其中操作最便利的是?Files?类,但它的性能不怎么好。如果对性能有要求就推荐使用带有缓存区的流来完成操作,如?BufferedWriter?或?BufferedOutputStream。如果写入的内容是字符串的话,那么推荐使用?BufferedWriter,如果写入的内容是二进制文件的话就推荐使用?BufferedOutputStream

参考 & 鸣谢

  • https://www.cnblogs.com/absfree/p/5415092.html


- EOF -




推荐阅读??点击标题可跳转

0、极客专属:几十款程序员秒懂的卫衣

1、没有几十年功力,写不出这一行“看似无用”的代码!!

2、Linus 已决定将 Rust 语言加入 Linux 内核

3、虾皮光速大裁员!上个厕所的功夫,瞬间查无此人...


关注「程序员的那些事」加星标,不错过圈内事

点赞和在看就是最大的支持?

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接