文件寫入的6種方法,這種方法性能最好

作者 | 王磊
來源 | Java中文社群(ID:javacn666)
在 Java 中操作文件的方法本質(zhì)上只有兩種:字符流和字節(jié)流,而字節(jié)流和字符流的實現(xiàn)類又有很多,因此在文件寫入時我們就可以選擇各種各樣的類來實現(xiàn)。我們本文就來盤點一下這些方法,順便測試一下它們性能,以便為我們選出最優(yōu)的寫入方法。
在正式開始之前,我們先來了解幾個基本的概念:流、字節(jié)流和字符流的定義與區(qū)別。
0.什么是流?
Java 中的“流”是一種抽象的概念,也是一種比喻,就好比水流一樣,水流是從一端流向另一端的,而在 Java 中的“水流”就是數(shù)據(jù),數(shù)據(jù)會從一端“流向”另一端。
根據(jù)流的方向性,我們可以將流分為輸入流和輸出流,當程序需要從數(shù)據(jù)源中讀入數(shù)據(jù)的時候就會開啟一個輸入流,相反,寫出數(shù)據(jù)到某個數(shù)據(jù)源目的地的時候也會開啟一個輸出流,數(shù)據(jù)源可以是文件、內(nèi)存或者網(wǎng)絡(luò)等。
1.什么是字節(jié)流?
字節(jié)流的基本單位為字節(jié)(Byte),一個字節(jié)通常為 8 位,它是用來處理二進制(數(shù)據(jù))的。字節(jié)流有兩個基類:InputStream(輸入字節(jié)流)和 OutputStream(輸出字節(jié)流)。
常用字節(jié)流的繼承關(guān)系圖如下圖所示:
其中 InputStream 用于讀操作,而 OutputStream 用于寫操作。
2.什么是字符流?
字符流的基本單位為 Unicode,大小為兩個字節(jié)(Byte),它通常用來處理文本數(shù)據(jù)。字符流的兩個基類:Reader(輸入字符流)和 Writer(輸出字符流)。
常用字符流的繼承關(guān)系圖如下圖所示:
3.流的分類
流可以根據(jù)不同的維度進行分類,比如可以根據(jù)流的方向進行分類,也可以根據(jù)傳輸?shù)膯挝贿M行分類,還可以根據(jù)流的功能進行分類,比如以下幾個。
① 按流向分類
輸出流: OutputStream和Writer?為基類。輸入流: InputStream和Reader為基類。
② 根據(jù)傳輸數(shù)據(jù)單位分類
字節(jié)流: OutputStream和InputStream為基類。字符流: Writer?和Reader?為基類。
③ 根據(jù)功能分類
字節(jié)流:可以從或向一個特定的地方(節(jié)點)讀寫數(shù)據(jù)。 處理流:是對一個已存在的流的連接和封裝,通過所封裝的流的功能調(diào)用實現(xiàn)數(shù)據(jù)讀寫。
PS:我們通常是以傳輸數(shù)據(jù)的單位來為流進行分類。
4.寫文件的6種方法
寫入文件的方法主要源于字符流 Writer?和輸出字節(jié)流 OutputStream?的子類,如下圖所示:
以上標注?號的類就是用來實現(xiàn)文件寫入的類,除此之外,在 JDK 1.7 中還提供了 Files?類用來實現(xiàn)對文件的各種操作,接下來我們分別來看。
方法 1:FileWriter
FileWriter?屬于「字符流」體系中的一員,也是文件寫入的基礎(chǔ)類,它包含 5 個構(gòu)造函數(shù),可以傳遞一個具體的文件位置,或者 File?對象,第二參數(shù)表示是否要追加文件,默認值為 false 表示重寫文件內(nèi)容,而非追加文件內(nèi)容(關(guān)于如何追加文件,我們后面會講)。

FileWriter 類的實現(xiàn)如下:
/**
??*?方法 1:使用 FileWriter 寫文件
??*?@param?filepath?文件目錄
??*?@param?content??待寫入內(nèi)容
??*?@throws?IOException
??*/
public?static?void?fileWriterMethod(String?filepath,?String?content)?throws?IOException?{
????try?(FileWriter?fileWriter?=?new?FileWriter(filepath))?{
????????fileWriter.append(content);
????}
}
只需要傳入具體的文件路徑和待寫入的內(nèi)容即可,調(diào)用代碼如下:
public?static?void?main(String[]?args)?{
????fileWriterMethod("/Users/mac/Downloads/io_test/write1.txt",?"哈嘍,Java中文社群.");
}
然后我們打開寫入的文件,實現(xiàn)結(jié)果如下:

關(guān)于資源釋放的問題:在 JDK 7 以上的版本,我們只需要使用 try-with-resource 的方式就可以實現(xiàn)資源的釋放,就比如使用 try (FileWriter fileWriter = new FileWriter(filepath)) {...} 就可以實現(xiàn) FileWriter 資源的自動釋放。
方法 2:BufferedWriter
BufferedWriter?也屬于字符流體系的一員,與 FileWriter?不同的是 BufferedWriter 自帶緩沖區(qū),因此它寫入文件的性能更高(下文會對二者進行測試)。
小知識點:緩沖區(qū)
緩沖區(qū)又稱為緩存,它是內(nèi)存空間的一部分。也就是說,在內(nèi)存空間中預留了一定的存儲空間,這些存儲空間用來緩沖輸入或輸出的數(shù)據(jù),這部分預留的空間就叫做緩沖區(qū)。
緩沖區(qū)的優(yōu)勢以文件流的寫入為例,如果我們不使用緩沖區(qū),那么每次寫操作 CPU 都會和低速存儲設(shè)備也就是磁盤進行交互,那么整個寫入文件的速度就會受制于低速的存儲設(shè)備(磁盤)。但如果使用緩沖區(qū)的話,每次寫操作會先將數(shù)據(jù)保存在高速緩沖區(qū)內(nèi)存上,當緩沖區(qū)的數(shù)據(jù)到達某個閾值之后,再將文件一次性寫入到磁盤上。因為內(nèi)存的寫入速度遠遠大于磁盤的寫入速度,所以當有了緩沖區(qū)之后,文件的寫入速度就被大大提升了。
了解了緩存區(qū)的優(yōu)點之后,咱們回到本文的主題,接下來我們用 BufferedWriter 來文件的寫入,實現(xiàn)代碼如下:
/**
?*?方法 2:使用 BufferedWriter 寫文件
?*?@param?filepath?文件目錄
?*?@param?content??待寫入內(nèi)容
?*?@throws?IOException
?*/
public?static?void?bufferedWriterMethod(String?filepath,?String?content)?throws?IOException?{
????try?(BufferedWriter?bufferedWriter?=?new?BufferedWriter(new?FileWriter(filepath)))?{
????????bufferedWriter.write(content);
????}
}
調(diào)用代碼和方法 1 類似,這里就不再贅述了。
方法 3:PrintWriter
PrintWriter?也屬于字符流體系中的一員,它雖然叫“字符打印流”,但使用它也可以實現(xiàn)文件的寫入,實現(xiàn)代碼如下:
/**
?*?方法 3:使用 PrintWriter 寫文件
?*?@param?filepath?文件目錄
?*?@param?content??待寫入內(nèi)容
?*?@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?類來完成調(diào)用。
方法 4:FileOutputStream
上面 3 個示例是關(guān)于字符流寫入文件的一些操作,而接下來我們將使用字節(jié)流來完成文件寫入。我們將使用 String?自帶的 getBytes() 方法先將字符串轉(zhuǎn)換成二進制文件,然后再進行文件寫入,它的實現(xiàn)代碼如下:
/**
?*?方法 4:使用 FileOutputStream 寫文件
?*?@param?filepath?文件目錄
?*?@param?content??待寫入內(nèi)容
?*?@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?屬于字節(jié)流體系中的一員,與 FileOutputStream?不同的是,它自帶了緩沖區(qū)的功能,因此性能更好,它的實現(xiàn)代碼如下:
/**
?*?方法 5:使用 BufferedOutputStream 寫文件
?*?@param?filepath?文件目錄
?*?@param?content??待寫入內(nèi)容
?*?@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?來實現(xiàn)文件的寫入。
Files?類是 JDK 7 添加的新的操作文件的類,它提供了提供了大量處理文件的方法,例如文件復制、讀取、寫入,獲取文件屬性、快捷遍歷文件目錄等,這些方法極大的方便了文件的操作,它的實現(xiàn)代碼如下:
/**
?*?方法 6:使用 Files 寫文件
?*?@param?filepath?文件目錄
?*?@param?content??待寫入內(nèi)容
?*?@throws?IOException
?*/
public?static?void?filesTest(String?filepath,?String?content)?throws?IOException?{
????Files.write(Paths.get(filepath),?content.getBytes());
}
以上這些方法都可以實現(xiàn)文件的寫入,那哪一種方法性能更高呢?接下來我們來測試一下。
5.性能測試
我們先來構(gòu)建一個比較大的字符串,然后分別用以上 6 種方法來測試文件寫入的速度,最后再把結(jié)果打印出來,測試代碼如下:
import?java.io.*;
import?java.nio.file.Files;
import?java.nio.file.Paths;
public?class?WriteExample?{
????public?static?void?main(String[]?args)?throws?IOException?{
????????//?構(gòu)建寫入內(nèi)容
????????StringBuilder?stringBuilder?=?new?StringBuilder();
????????for?(int?i?=?0;?i?1000000;?i++)?{
????????????stringBuilder.append("ABCDEFGHIGKLMNOPQRSEUVWXYZ");
????????}
????????//?寫入內(nèi)容
????????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??待寫入內(nèi)容
?????*?@throws?IOException
?????*/
????private?static?void?filesTest(String?filepath,?String?content)?throws?IOException?{
????????Files.write(Paths.get(filepath),?content.getBytes());
????}
????/**
?????*?方法五:使用?BufferedOutputStream?寫文件
?????*?@param?filepath?文件目錄
?????*?@param?content??待寫入內(nèi)容
?????*?@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??待寫入內(nèi)容
?????*?@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??待寫入內(nèi)容
?????*?@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??待寫入內(nèi)容
?????*?@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??待寫入內(nèi)容
?????*?@throws?IOException
?????*/
????private?static?void?fileWriterTest(String?filepath,?String?content)?throws?IOException?{
????????try?(FileWriter?fileWriter?=?new?FileWriter(filepath))?{
????????????fileWriter.append(content);
????????}
????}
}
在查看結(jié)果之前,我們先去對應(yīng)的文件夾看看寫入的文件是否正常,如下圖所示:
從上述結(jié)果可以看出,每種方法都正常寫入了 26 MB 的數(shù)據(jù),它們最終執(zhí)行的結(jié)果如下圖所示:
從以上結(jié)果可以看出,字符流的操作速度最快,這是因為我們本次測試的代碼操作的是字符串,所以在使用字節(jié)流時,需要先將字符串轉(zhuǎn)換為字節(jié)流,因此在執(zhí)行效率上不占優(yōu)勢。
從上述結(jié)果可以看出,性能最好的是帶有緩沖區(qū)的字符串寫入流 BufferedWriter,性能最慢的是 Files。
PS:以上的測試結(jié)果只是針對字符串的操作場景有效,如果操作的是二進制的文件,那么就應(yīng)該使用帶緩沖區(qū)的字節(jié)流 BufferedOutputStream。
6.擴展知識:內(nèi)容追加
以上代碼會對文件進行重寫,如果只想在原有的基礎(chǔ)上追加內(nèi)容,就需要在創(chuàng)建寫入流的時候多設(shè)置一個 append?的參數(shù)為 true,比如如果我們使用 FileWriter?來實現(xiàn)文件的追加的話,實現(xiàn)代碼是這樣的:
public?static?void?fileWriterMethod(String?filepath,?String?content)?throws?IOException?{
????//?第二個?append?的參數(shù)傳遞一個?true?=?追加文件的意思
????try?(FileWriter?fileWriter?=?new?FileWriter(filepath,?true))?{
????????fileWriter.append(content);
????}
}
如果使用的是 BufferedWriter?或 PrintWriter,也是需要在構(gòu)建 new FileWriter?類時多設(shè)置一個 append 的參數(shù)為 true,實現(xiàn)代碼如下:
try?(BufferedWriter?bufferedWriter?=?new?BufferedWriter(
????new?FileWriter(filepath,?true)))?{
????bufferedWriter.write(content);
}
相比來說 Files 類要想實現(xiàn)文件的追加寫法更加特殊一些,它需要在調(diào)用 write 方法時多傳一個 StandardOpenOption.APPEND 的參數(shù),它的實現(xiàn)代碼如下:
Files.write(Paths.get(filepath),?content.getBytes(),?StandardOpenOption.APPEND);
7.總結(jié)
本文我們展示了 6 種寫入文件的方法,這 6 種方法總共分為 3 類:字符流寫入、字節(jié)流寫入和 Files?類寫入。其中操作最便利的是 Files 類,但它的性能不怎么好。如果對性能有要求就推薦使用帶有緩存區(qū)的流來完成操作,如 BufferedWriter?或 BufferedOutputStream。如果寫入的內(nèi)容是字符串的話,那么推薦使用 BufferedWriter,如果寫入的內(nèi)容是二進制文件的話就推薦使用 BufferedOutputStream。
參考 & 鳴謝
https://www.cnblogs.com/absfree/p/5415092.html

