共计 8924 个字符,预计需要花费 23 分钟才能阅读完成。
在 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
关注公众号「Java 中文社群」发现更多干货。