在 Java 中操作文件的方法本质上只有两种:字符流和字节流,而字节流和字符流的实现类又有很多,因此在文件写入时我们就可以选择各种各样的类来实现。
流
Java 中的“流”是一种抽象的概念,也是一种比喻,就好比水流一样,水流是从一端流向另一端的,而在 Java 中的“水流”就是数据,数据会从一端“流向”另一端。
根据流的方向性,可以将流分为输入流和输出流,当程序需要从数据源中读入数据的时候就会开启一个输入流,相反,写出数据到某个数据源目的地的时候也会开启一个输出流,数据源可以是文件、内存或者网络等。
字节流
字节流的基本单位为字节(Byte),一个字节通常为 8 位,它是用来处理二进制(数据)的。字节流有两个基类:InputStream(输入字节流)和 OutputStream(输出字节流)。
常用字节流的继承关系图如下图所示:
其中 InputStream 用于读操作,而 OutputStream 用于写操作。
字符流
字符流的基本单位为 Unicode,大小为两个字节(Byte),它通常用来处理文本数据。字符流的两个基类:Reader(输入字符流)和 Writer(输出字符流)。
常用字符流的继承关系图如下图所示:
流的分类
流可以根据不同的维度进行分类,比如可以根据流的方向进行分类,也可以根据传输的单位进行分类,还可以根据流的功能进行分类,比如以下几个。
① 按流向分类
- 输出流:OutputStream 和 Writer 为基类。
- 输入流:InputStream 和 Reader 为基类。
② 根据传输数据单位分类
- 字节流:OutputStream 和 InputStream 为基类。
- 字符流:Writer 和 Reader 为基类。
③ 根据功能分类
- 字节流:可以从或向一个特定的地方(节点)读写数据。
- 处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。
PS:我们通常是以传输数据的单位来为流进行分类。
写文件的6种方法
写入文件的方法主要源于字符流 Writer 和输出字节流 OutputStream 的子类,如下图所示:
以上标注✅号的类就是用来实现文件写入的类,除此之外,在 JDK 1.7 中还提供了Files 类用来实现对文件的各种操作。
①FileWriter
FileWriter 属于「字符流」体系中的一员,也是文件写入的基础类,它包含 5 个构造函数,可以传递一个具体的文件位置,或者 File 对象,第二参数表示是否要追加文件,默认值为 false 表示重写文件内容,而非追加文件内容。
FileWriter 类的实现如下:
/**
* 方法 1:使用 FileWriter 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/publicstaticvoidfileWriterMethod(String filepath, String content)throws IOException{try(FileWriter fileWriter=newFileWriter(filepath)){
fileWriter.append(content);}}
只需要传入具体的文件路径和待写入的内容即可,调用代码如下:
publicstaticvoidmain(String[] args){fileWriterMethod("/Users/mac/Downloads/io_test/write1.txt","哈喽");}
关于资源释放的问题:在 JDK 7 以上的版本,只需要使用 try-with-resource 的方式就可以实现资源的释放,就比如使用 try (FileWriter fileWriter = new FileWriter(filepath)) {…} 就可以实现 FileWriter 资源的自动释放。
②BufferedWriter
BufferedWriter 也属于字符流体系的一员,与 FileWriter 不同的是 BufferedWriter自带缓冲区,因此它写入文件的性能更高。
缓冲区
缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。
缓冲区的优势以文件流的写入为例,如果不使用缓冲区,那么每次写操作 CPU 都会和低速存储设备也就是磁盘进行交互,那么整个写入文件的速度就会受制于低速的存储设备(磁盘)。但如果使用缓冲区的话,每次写操作会先将数据保存在高速缓冲区内存上,当缓冲区的数据到达某个阈值之后,再将文件一次性写入到磁盘上。因为内存的写入速度远远大于磁盘的写入速度,所以当有了缓冲区之后,文件的写入速度就被大大提升了。
BufferedWriter 来文件的写入,实现代码如下:
/**
* 方法 2:使用 BufferedWriter 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/publicstaticvoidbufferedWriterMethod(String filepath, String content)throws IOException{try(BufferedWriter bufferedWriter=newBufferedWriter(newFileWriter(filepath))){
bufferedWriter.write(content);}}
③PrintWriter
PrintWriter 也属于字符流体系中的一员,它虽然叫“字符打印流”,但使用它也可以实现文件的写入,实现代码如下:
/**
* 方法 3:使用 PrintWriter 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/publicstaticvoidprintWriterMethod(String filepath, String content)throws IOException{try(PrintWriter printWriter=newPrintWriter(newFileWriter(filepath))){
printWriter.print(content);}}
无论是 PrintWriter 还是 BufferedWriter 都必须基于 FileWriter 类来完成调用。
④FileOutputStream
使用 String 自带的 getBytes() 方法先将字符串转换成二进制文件,然后再进行文件写入(字节流),它的实现代码如下:
/**
* 方法 4:使用 FileOutputStream 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/publicstaticvoidfileOutputStreamMethod(String filepath, String content)throws IOException{try(FileOutputStream fileOutputStream=newFileOutputStream(filepath)){byte[] bytes= content.getBytes();
fileOutputStream.write(bytes);}}
⑤BufferedOutputStream
BufferedOutputStream 属于字节流体系中的一员,与 FileOutputStream 不同的是,它自带了缓冲区的功能,因此性能更好,它的实现代码如下:
/**
* 方法 5:使用 BufferedOutputStream 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/publicstaticvoidbufferedOutputStreamMethod(String filepath, String content)throws IOException{try(BufferedOutputStream bufferedOutputStream=newBufferedOutputStream(newFileOutputStream(filepath))){
bufferedOutputStream.write(content.getBytes());}}
⑥Files
使用 JDK 7 中提供的一个新的文件操作类 Files 来实现文件的写入。
Files 类是 JDK 7 添加的新的操作文件的类,它提供了提供了大量处理文件的方法,例如文件复制、读取、写入,获取文件属性、快捷遍历文件目录等,这些方法极大的方便了文件的操作,它的实现代码如下:
/**
* 方法 6:使用 Files 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/publicstaticvoidfilesTest(String filepath, String content)throws IOException{
Files.write(Paths.get(filepath), content.getBytes());}
性能测试
构建一个比较大的字符串,然后分别用以上 6 种方法来测试文件写入的速度,最后再把结果打印出来,测试代码如下:
import java.io.*;import java.nio.file.Files;import java.nio.file.Paths;publicclassWriteExample{publicstaticvoidmain(String[] args)throws IOException{// 构建写入内容
StringBuilder stringBuilder=newStringBuilder();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
*/privatestaticvoidfilesTest(String filepath, String content)throws IOException{
Files.write(Paths.get(filepath), content.getBytes());}/**
* 方法五:使用 BufferedOutputStream 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/privatestaticvoidbufferedOutputStreamTest(String filepath, String content)throws IOException{try(BufferedOutputStream bufferedOutputStream=newBufferedOutputStream(newFileOutputStream(filepath))){
bufferedOutputStream.write(content.getBytes());}}/**
* 方法四:使用 FileOutputStream 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/privatestaticvoidfileOutputStreamTest(String filepath, String content)throws IOException{try(FileOutputStream fileOutputStream=newFileOutputStream(filepath)){byte[] bytes= content.getBytes();
fileOutputStream.write(bytes);}}/**
* 方法三:使用 PrintWriter 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/privatestaticvoidprintWriterTest(String filepath, String content)throws IOException{try(PrintWriter printWriter=newPrintWriter(newFileWriter(filepath))){
printWriter.print(content);}}/**
* 方法二:使用 BufferedWriter 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/privatestaticvoidbufferedWriterTest(String filepath, String content)throws IOException{try(BufferedWriter bufferedWriter=newBufferedWriter(newFileWriter(filepath))){
bufferedWriter.write(content);}}/**
* 方法一:使用 FileWriter 写文件
* @param filepath 文件目录
* @param content 待写入内容
* @throws IOException
*/privatestaticvoidfileWriterTest(String filepath, String content)throws IOException{try(FileWriter fileWriter=newFileWriter(filepath)){
fileWriter.append(content);}}}
在查看结果之前,我们先去对应的文件夹看看写入的文件是否正常,如下图所示:
从上述结果可以看出,每种方法都正常写入了 26 MB 的数据,它们最终执行的结果如下图所示:
从以上结果可以看出,字符流的操作速度最快,这是因为本次测试的代码操作的是字符串,所以在使用字节流时,需要先将字符串转换为字节流,因此在执行效率上不占优势。
从上述结果可以看出,性能最好的是带有缓冲区的字符串写入流 BufferedWriter,性能最慢的是 Files。
PS:以上的测试结果只是针对字符串的操作场景有效,如果操作的是二进制的文件,那么就应该使用带缓冲区的字节流 BufferedOutputStream。
内容追加
如果只想在原有的基础上追加内容,就需要在创建写入流的时候多设置一个 append 的参数为 true,比如使用 FileWriter 来实现文件的追加的话,实现代码是这样的:
publicstaticvoidfileWriterMethod(String filepath, String content)throws IOException{// 第二个 append 的参数传递一个 true = 追加文件的意思try(FileWriter fileWriter=newFileWriter(filepath,true)){
fileWriter.append(content);}}
如果使用的是 BufferedWriter 或 PrintWriter,也是需要在构建 new FileWriter 类时多设置一个 append 的参数为 true,实现代码如下:
try(BufferedWriter bufferedWriter=newBufferedWriter(newFileWriter(filepath,true))){
bufferedWriter.write(content);}
相比来说 Files 类要想实现文件的追加写法更加特殊一些,它需要在调用 write 方法时多传一个 StandardOpenOption.APPEND 的参数,它的实现代码如下:
Files.write(Paths.get(filepath), content.getBytes(), StandardOpenOption.APPEND);
7.总结
这 6 种方法总共分为 3 类:字符流写入、字节流写入和 Files 类写入。其中操作最便利的是 Files 类,但它的性能不怎么好。如果对性能有要求就推荐使用带有缓存区的流来完成操作,如 BufferedWriter 或 BufferedOutputStream。如果写入的内容是字符串的话,那么推荐使用 BufferedWriter,如果写入的内容是二进制文件的话就推荐使用 BufferedOutputStream。