文件写入的6种方法,你觉得哪种性能最好?

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

[[359184]]

在 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. /** 
  2.   * 方法 1:使用 FileWriter 写文件 
  3.   * @param filepath 文件目录 
  4.   * @param content  待写入内容 
  5.   * @throws IOException 
  6.   */ 
  7. public static void fileWriterMethod(String filepath, String content) throws IOException { 
  8.     try (FileWriter fileWriter = new FileWriter(filepath)) { 
  9.         fileWriter.append(content); 
  10.     } 

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

  1. public static void main(String[] args) { 
  2.     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 来文件的写入,实现代码如下:

  1. /** 
  2.  * 方法 2:使用 BufferedWriter 写文件 
  3.  * @param filepath 文件目录 
  4.  * @param content  待写入内容 
  5.  * @throws IOException 
  6.  */ 
  7. public static void bufferedWriterMethod(String filepath, String content) throws IOException { 
  8.     try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filepath))) { 
  9.         bufferedWriter.write(content); 
  10.     } 

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

方法 3:PrintWriter

PrintWriter 也属于字符流体系中的一员,它虽然叫“字符打印流”,但使用它也可以实现文件的写入,实现代码如下:

  1. /** 
  2.  * 方法 3:使用 PrintWriter 写文件 
  3.  * @param filepath 文件目录 
  4.  * @param content  待写入内容 
  5.  * @throws IOException 
  6.  */ 
  7. public static void printWriterMethod(String filepath, String content) throws IOException { 
  8.     try (PrintWriter printWriter = new PrintWriter(new FileWriter(filepath))) { 
  9.         printWriter.print(content); 
  10.     } 

从上述代码可以看出,无论是 PrintWriter 还是 BufferedWriter 都必须基于 FileWriter 类来完成调用。

方法 4:FileOutputStream

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

  1. /** 
  2.  * 方法 4:使用 FileOutputStream 写文件 
  3.  * @param filepath 文件目录 
  4.  * @param content  待写入内容 
  5.  * @throws IOException 
  6.  */ 
  7. public static void fileOutputStreamMethod(String filepath, String content) throws IOException { 
  8.     try (FileOutputStream fileOutputStream = new FileOutputStream(filepath)) { 
  9.         byte[] bytes = content.getBytes(); 
  10.         fileOutputStream.write(bytes); 
  11.     } 

方法 5:BufferedOutputStream

BufferedOutputStream 属于字节流体系中的一员,与 FileOutputStream 不同的是,它自带了缓冲区的功能,因此性能更好,它的实现代码如下:

  1. /** 
  2.  * 方法 5:使用 BufferedOutputStream 写文件 
  3.  * @param filepath 文件目录 
  4.  * @param content  待写入内容 
  5.  * @throws IOException 
  6.  */ 
  7. public static void bufferedOutputStreamMethod(String filepath, String content) throws IOException { 
  8.     try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream( 
  9.             new FileOutputStream(filepath))) { 
  10.         bufferedOutputStream.write(content.getBytes()); 
  11.     } 

方法 6:Files

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

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

  1. /** 
  2.  * 方法 6:使用 Files 写文件 
  3.  * @param filepath 文件目录 
  4.  * @param content  待写入内容 
  5.  * @throws IOException 
  6.  */ 
  7. public static void filesTest(String filepath, String content) throws IOException { 
  8.     Files.write(Paths.get(filepath), content.getBytes()); 

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

5.性能测试

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

  1. import java.io.*; 
  2. import java.nio.file.Files; 
  3. import java.nio.file.Paths; 
  4.  
  5. public class WriteExample { 
  6.     public static void main(String[] args) throws IOException { 
  7.         // 构建写入内容 
  8.         StringBuilder stringBuilder = new StringBuilder(); 
  9.         for (int i = 0; i < 1000000; i++) { 
  10.             stringBuilder.append("ABCDEFGHIGKLMNOPQRSEUVWXYZ"); 
  11.         } 
  12.         // 写入内容 
  13.         final String content = stringBuilder.toString(); 
  14.         // 存放文件的目录 
  15.         final String filepath1 = "/Users/mac/Downloads/io_test/write1.txt"
  16.         final String filepath2 = "/Users/mac/Downloads/io_test/write2.txt"
  17.         final String filepath3 = "/Users/mac/Downloads/io_test/write3.txt"
  18.         final String filepath4 = "/Users/mac/Downloads/io_test/write4.txt"
  19.         final String filepath5 = "/Users/mac/Downloads/io_test/write5.txt"
  20.         final String filepath6 = "/Users/mac/Downloads/io_test/write6.txt"
  21.  
  22.         // 方法一:使用 FileWriter 写文件 
  23.         long stime1 = System.currentTimeMillis(); 
  24.         fileWriterTest(filepath1, content); 
  25.         long etime1 = System.currentTimeMillis(); 
  26.         System.out.println("FileWriter 写入用时:" + (etime1 - stime1)); 
  27.  
  28.         // 方法二:使用 BufferedWriter 写文件 
  29.         long stime2 = System.currentTimeMillis(); 
  30.         bufferedWriterTest(filepath2, content); 
  31.         long etime2 = System.currentTimeMillis(); 
  32.         System.out.println("BufferedWriter 写入用时:" + (etime2 - stime2)); 
  33.  
  34.         // 方法三:使用 PrintWriter 写文件 
  35.         long stime3 = System.currentTimeMillis(); 
  36.         printWriterTest(filepath3, content); 
  37.         long etime3 = System.currentTimeMillis(); 
  38.         System.out.println("PrintWriterTest 写入用时:" + (etime3 - stime3)); 
  39.  
  40.         // 方法四:使用 FileOutputStream  写文件 
  41.         long stime4 = System.currentTimeMillis(); 
  42.         fileOutputStreamTest(filepath4, content); 
  43.         long etime4 = System.currentTimeMillis(); 
  44.         System.out.println("FileOutputStream 写入用时:" + (etime4 - stime4)); 
  45.  
  46.         // 方法五:使用 BufferedOutputStream 写文件 
  47.         long stime5 = System.currentTimeMillis(); 
  48.         bufferedOutputStreamTest(filepath5, content); 
  49.         long etime5 = System.currentTimeMillis(); 
  50.         System.out.println("BufferedOutputStream 写入用时:" + (etime5 - stime5)); 
  51.  
  52.         // 方法六:使用 Files 写文件 
  53.         long stime6 = System.currentTimeMillis(); 
  54.         filesTest(filepath6, content); 
  55.         long etime6 = System.currentTimeMillis(); 
  56.         System.out.println("Files 写入用时:" + (etime6 - stime6)); 
  57.  
  58.     } 
  59.  
  60.     /** 
  61.      * 方法六:使用 Files 写文件 
  62.      * @param filepath 文件目录 
  63.      * @param content  待写入内容 
  64.      * @throws IOException 
  65.      */ 
  66.     private static void filesTest(String filepath, String content) throws IOException { 
  67.         Files.write(Paths.get(filepath), content.getBytes()); 
  68.     } 
  69.  
  70.     /** 
  71.      * 方法五:使用 BufferedOutputStream 写文件 
  72.      * @param filepath 文件目录 
  73.      * @param content  待写入内容 
  74.      * @throws IOException 
  75.      */ 
  76.     private static void bufferedOutputStreamTest(String filepath, String content) throws IOException { 
  77.         try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream( 
  78.                 new FileOutputStream(filepath))) { 
  79.             bufferedOutputStream.write(content.getBytes()); 
  80.         } 
  81.     } 
  82.  
  83.     /** 
  84.      * 方法四:使用 FileOutputStream  写文件 
  85.      * @param filepath 文件目录 
  86.      * @param content  待写入内容 
  87.      * @throws IOException 
  88.      */ 
  89.     private static void fileOutputStreamTest(String filepath, String content) throws IOException { 
  90.         try (FileOutputStream fileOutputStream = new FileOutputStream(filepath)) { 
  91.             byte[] bytes = content.getBytes(); 
  92.             fileOutputStream.write(bytes); 
  93.         } 
  94.     } 
  95.  
  96.     /** 
  97.      * 方法三:使用 PrintWriter 写文件 
  98.      * @param filepath 文件目录 
  99.      * @param content  待写入内容 
  100.      * @throws IOException 
  101.      */ 
  102.     private static void printWriterTest(String filepath, String content) throws IOException { 
  103.         try (PrintWriter printWriter = new PrintWriter(new FileWriter(filepath))) { 
  104.             printWriter.print(content); 
  105.         } 
  106.     } 
  107.  
  108.     /** 
  109.      * 方法二:使用 BufferedWriter 写文件 
  110.      * @param filepath 文件目录 
  111.      * @param content  待写入内容 
  112.      * @throws IOException 
  113.      */ 
  114.     private static void bufferedWriterTest(String filepath, String content) throws IOException { 
  115.         try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filepath))) { 
  116.             bufferedWriter.write(content); 
  117.         } 
  118.     } 
  119.  
  120.     /** 
  121.      * 方法一:使用 FileWriter 写文件 
  122.      * @param filepath 文件目录 
  123.      * @param content  待写入内容 
  124.      * @throws IOException 
  125.      */ 
  126.     private static void fileWriterTest(String filepath, String content) throws IOException { 
  127.         try (FileWriter fileWriter = new FileWriter(filepath)) { 
  128.             fileWriter.append(content); 
  129.         } 
  130.     } 

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


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


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

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

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

6.扩展知识:内容追加

以上代码会对文件进行重写,如果只想在原有的基础上追加内容,就需要在创建写入流的时候多设置一个 append 的参数为 true,比如如果我们使用 FileWriter 来实现文件的追加的话,实现代码是这样的:

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

如果使用的是 BufferedWriter 或 PrintWriter,也是需要在构建 new FileWriter 类时多设置一个 append 的参数为 true,实现代码如下:

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

相比来说 Files 类要想实现文件的追加写法更加特殊一些,它需要在调用 write 方法时多传一个 StandardOpenOption.APPEND 的参数,它的实现代码如下:

  1. 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

 

责任编辑:姜华 来源: Java中文社群
相关推荐

2024-01-31 09:24:58

2023-10-30 09:46:08

接口重试技巧

2010-04-30 10:40:19

Oracle实现

2010-08-26 09:16:42

Web服务器

2023-09-07 15:11:44

2020-06-23 09:52:31

运营效率首席信息官IT预算

2020-05-15 10:09:17

优化创新数字化转型CIO

2024-07-30 10:45:05

容器化编排工具

2015-03-13 13:50:47

Java读取文件夹大小Java读取文件Java读取

2009-04-01 11:17:52

内存分配调优Oracle

2010-07-21 16:01:33

升职

2014-11-04 09:57:30

Windows

2013-10-21 14:26:04

2019-08-22 09:57:10

ExpdpImpdpMOS

2009-04-07 10:52:00

职场工作方法

2017-11-03 10:40:25

Python复制文件方法

2018-01-04 09:46:48

PHPHTTP

2022-11-10 14:33:40

Kubernetes容器

2016-10-12 13:53:38

JavaByteBufferRandomAcces

2021-02-05 08:03:52

Java
点赞
收藏

51CTO技术栈公众号