1. 程式人生 > >檔案寫入的6種方法,這種方法效能最好

檔案寫入的6種方法,這種方法效能最好

在 Java 中操作檔案的方法本質上只有兩種:**字元流和位元組流**,而位元組流和字元流的實現類又有很多,因此在檔案寫入時我們就可以選擇各種各樣的類來實現。我們本文就來盤點一下這些方法,順便測試一下它們效能,以便為我們選出最優的寫入方法。 在正式開始之前,我們先來了解幾個基本的概念:流、位元組流和字元流的定義與區別。 # 0.什麼是流? Java 中的“流”是一種抽象的概念,也是一種比喻,就好比水流一樣,水流是從一端流向另一端的,而在 Java 中的“水流”就是資料,資料會從一端“流向”另一端。 根據流的方向性,我們可以將流分為**輸入流和輸出流**,當程式需要從資料來源中讀入資料的時候就會開啟一個輸入流,相反,寫出資料到某個資料來源目的地的時候也會開啟一個輸出流,資料來源可以是檔案、記憶體或者網路等。 # 1.什麼是位元組流? 位元組流的基本單位為位元組(Byte),一個位元組通常為 8 位,它是用來處理二進位制(資料)的。位元組流有兩個基類:`InputStream`(輸入位元組流)和 `OutputStream`(輸出位元組流)。 常用位元組流的繼承關係圖如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1608437436866-f4c15c3e-931e-4558-bf34-a8acf955f665.png#align=left&display=inline&height=418&margin=%5Bobject%20Object%5D&name=image.png&originHeight=836&originWidth=1598&size=102265&status=done&style=none&width=799) 其中 `InputStream` 用於讀操作,而 `OutputStream` 用於寫操作。 # 2.什麼是字元流? 字元流的基本單位為 Unicode,大小為兩個位元組(Byte),它通常用來處理文字資料。字元流的兩個基類:`Reader`(輸入字元流)和 `Writer`(輸出字元流)。 常用字元流的繼承關係圖如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1608473099441-12f5294a-0d00-4407-bfe0-332b28e8dedf.png#align=left&display=inline&height=503&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1006&originWidth=1412&size=96967&status=done&style=none&width=706) # 3.流的分類 流可以根據不同的維度進行分類,比如可以根據流的方向進行分類,也可以根據傳輸的單位進行分類,還可以根據流的功能進行分類,比如以下幾個。 ### ① 按流向分類 - 輸出流:`OutputStream` 和 `Writer` 為基類。 - 輸入流:`InputStream` 和 `Reader` 為基類。 ### ② 根據傳輸資料單位分類 - 位元組流:`OutputStream` 和 `InputStream` 為基類。 - 字元流:`Writer` 和 `Reader` 為基類。 ### ③ 根據功能分類 - 位元組流:可以從或向一個特定的地方(節點)讀寫資料。 - 處理流:是對一個已存在的流的連線和封裝,通過所封裝的流的功能呼叫實現資料讀寫。 > PS:我們通常是以傳輸資料的單位來為流進行分類。 # 4.寫檔案的6種方法 寫入檔案的方法主要源於字元流 `Writer` 和輸出位元組流 `OutputStream` 的子類,如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1608473865157-bcd30423-bbe2-4198-bf69-26af2e4f0a35.png#align=left&display=inline&height=872&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1744&originWidth=1892&size=218567&status=done&style=none&width=946) 以上標註✅號的類就是用來實現檔案寫入的類,除此之外,在 JDK 1.7 中還提供了 `Files` 類用來實現對檔案的各種操作,接下來我們分別來看。 ## 方法 1:FileWriter `FileWriter` 屬於**「字元流」體系中的一員**,也是檔案寫入的基礎類,它包含 5 個建構函式,可以傳遞一個具體的檔案位置,或者 `File` 物件,第二引數表示是否要追加檔案,預設值為 `false` 表示重寫檔案內容,而非追加檔案內容(關於如何追加檔案,我們後面會講)。 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1608474396477-8bbb6a30-7e9a-4bb1-8d74-ea4b3ce2d910.png#align=left&display=inline&height=155&margin=%5Bobject%20Object%5D&name=image.png&originHeight=310&originWidth=542&size=31576&status=done&style=none&width=271) `FileWriter` 類的實現如下: ```java /** * 方法 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); } } ``` 只需要傳入具體的檔案路徑和待寫入的內容即可,呼叫程式碼如下: ```java public static void main(String[] args) { fileWriterMethod("/Users/mac/Downloads/io_test/write1.txt", "哈嘍,Java中文社群."); } ``` 然後我們開啟寫入的檔案,實現結果如下: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1608474750253-b411fd18-818e-49b1-946b-4f74140fb6ba.png#align=left&display=inline&height=96&margin=%5Bobject%20Object%5D&name=image.png&originHeight=192&originWidth=684&size=18013&status=done&style=none&width=342) > 關於資源釋放的問題:在 JDK 7 以上的版本,我們只需要使用 try-with-resource 的方式就可以實現資源的釋放,就比如使用 try (FileWriter fileWriter = new FileWriter(filepath)) {...} 就可以實現 FileWriter 資源的自動釋放。 ## 方法 2:BufferedWriter `BufferedWriter` 也屬於字元流體系的一員,與 `FileWriter` 不同的是 `BufferedWriter` **自帶緩衝區**,因此它寫入檔案的效能更高(下文會對二者進行測試)。 ### 小知識點:緩衝區 緩衝區又稱為快取,它是記憶體空間的一部分。也就是說,在記憶體空間中預留了一定的儲存空間,這些儲存空間用來緩衝輸入或輸出的資料,這部分預留的空間就叫做緩衝區。 **緩衝區的優勢** 以檔案流的寫入為例,如果我們不使用緩衝區,那麼每次寫操作 CPU 都會和低速儲存裝置也就是磁碟進行互動,那麼整個寫入檔案的速度就會受制於低速的儲存裝置(磁碟)。但如果使用緩衝區的話,每次寫操作會先將資料儲存在高速緩衝區記憶體上,當緩衝區的資料到達某個閾值之後,再將檔案一次性寫入到磁碟上。因為記憶體的寫入速度遠遠大於磁碟的寫入速度,所以當有了緩衝區之後,檔案的寫入速度就被大大提升了。 瞭解了快取區的優點之後,咱們回到本文的主題,接下來我們用 `BufferedWriter` 來檔案的寫入,實現程式碼如下: ```java /** * 方法 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` 也屬於字元流體系中的一員,它雖然叫“字元列印流”,但使用它也可以實現檔案的寫入,實現程式碼如下: ```java /** * 方法 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()` 方法先將字串轉換成二進位制檔案,然後再進行檔案寫入,它的實現程式碼如下: ```java /** * 方法 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` 不同的是,它自帶了緩衝區的功能,因此效能更好,它的實現程式碼如下: ```java /** * 方法 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 新增的新的操作檔案的類,它提供了提供了大量處理檔案的方法,例如檔案複製、讀取、寫入,獲取檔案屬性、快捷遍歷檔案目錄等,這些方法極大的方便了檔案的操作,它的實現程式碼如下: ```java /** * 方法 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 種方法來測試檔案寫入的速度,最後再把結果打印出來,測試程式碼如下: ```java 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); } } } ``` 在檢視結果之前,我們先去對應的資料夾看看寫入的檔案是否正常,如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1608433227017-82b31bf0-3600-4ad2-a1a1-b17db50642cf.png#align=left&display=inline&height=117&margin=%5Bobject%20Object%5D&name=image.png&originHeight=233&originWidth=696&size=35860&status=done&style=none&width=348) 從上述結果可以看出,每種方法都正常寫入了 26 MB 的資料,它們最終執行的結果如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1608472439443-a5c7020e-ad75-46bc-926d-a0f5cd30ef10.png#align=left&display=inline&height=183&margin=%5Bobject%20Object%5D&name=image.png&originHeight=366&originWidth=748&size=42462&status=done&style=none&width=374) 從以上結果可以看出,字元流的操作速度最快,這是因為我們本次測試的程式碼操作的是字串,所以在使用位元組流時,需要先將字串轉換為位元組流,因此在執行效率上不佔優勢。 從上述結果可以看出,**效能最好的是帶有緩衝區的字串寫入流 `BufferedWriter`,效能最慢的是 `Files`**。 >
PS:以上的測試結果只是針對字串的操作場景有效,如果操作的是二進位制的檔案,那麼就應該使用帶緩衝區的位元組流 BufferedOutputStream。 # 6.擴充套件知識:內容追加 以上程式碼會對檔案進行重寫,如果只想在原有的基礎上追加內容,就需要在建立寫入流的時候多設定一個 `append` 的引數為 `true`,比如如果我們使用 `FileWriter` 來實現檔案的追加的話,實現程式碼是這樣的: ```java 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`,實現程式碼如下: ```java try (BufferedWriter bufferedWriter = new BufferedWriter( new FileWriter(filepath, true))) { bufferedWriter.write(content); } ``` 相比來說 `Files` 類要想實現檔案的追加寫法更加特殊一些,它需要在呼叫 `write` 方法時多傳一個 `StandardOpenOption.APPEND` 的引數,它的實現程式碼如下: ```java 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](https://www.cnblogs.com/absfree/p/5415092.html) >
關注公眾號「Java中文社群」發現更多