別亂用了!文件寫入的 6 種方法

在 Java 中操作文件的方法本質上只有兩種:字符流和字節流,而字節流和字符流的實現類又有很多,因此在文件寫入時我們就可以選擇各種各樣的類來實現。我們本文就來盤點一下這些方法,順便測試一下它們性能,以便爲我們選出最優的寫入方法。

在正式開始之前,我們先來了解幾個基本的概念:流、字節流和字符流的定義與區別。

  1. 什麼是流?

Java 中的 “流” 是一種抽象的概念,也是一種比喻,就好比水流一樣,水流是從一端流向另一端的,而在 Java 中的 “水流” 就是數據,數據會從一端 “流向” 另一端。

根據流的方向性,我們可以將流分爲輸入流和輸出流,當程序需要從數據源中讀入數據的時候就會開啓一個輸入流,相反,寫出數據到某個數據源目的地的時候也會開啓一個輸出流,數據源可以是文件、內存或者網絡等。

  1. 什麼是字節流?

字節流的基本單位爲字節(Byte),一個字節通常爲 8 位,它是用來處理二進制(數據)的。字節流有兩個基類:InputStream(輸入字節流)和 OutputStream(輸出字節流)。

常用字節流的繼承關係圖如下圖所示:

其中 InputStream 用於讀操作,而 OutputStream 用於寫操作。

  1. 什麼是字符流?

字符流的基本單位爲 Unicode,大小爲兩個字節(Byte),它通常用來處理文本數據。字符流的兩個基類:Reader(輸入字符流)和 Writer(輸出字符流)。

常用字符流的繼承關係圖如下圖所示:

  1. 流的分類

流可以根據不同的維度進行分類,比如可以根據流的方向進行分類,也可以根據傳輸的單位進行分類,還可以根據流的功能進行分類,比如以下幾個。

① 按流向分類

② 根據傳輸數據單位分類

③ 根據功能分類

PS:我們通常是以傳輸數據的單位來爲流進行分類。

  1. 寫文件的 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());  
}

以上這些方法都可以實現文件的寫入,那哪一種方法性能更高呢?接下來我們來測試一下。

  1. 性能測試

我們先來構建一個比較大的字符串,然後分別用以上 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

  1. 擴展知識:內容追加

以上代碼會對文件進行重寫,如果只想在原有的基礎上追加內容,就需要在創建寫入流的時候多設置一個 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);
  1. 總結

本文我們展示了 6 種寫入文件的方法,這 6 種方法總共分爲 3 類:字符流寫入、字節流寫入和 Files 類寫入。其中操作最便利的是 Files 類,但它的性能不怎麼好。如果對性能有要求就推薦使用帶有緩存區的流來完成操作,如 BufferedWriter 或 BufferedOutputStream。如果寫入的內容是字符串的話,那麼推薦使用 BufferedWriter,如果寫入的內容是二進制文件的話就推薦使用 BufferedOutputStream

參考 & 鳴謝

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1WkBuYqYZFJyIbU3VL_CsA