FileOutputStream 與 FileChannel 指南
一、簡介
在 Java 中處理檔案 I/O 作業時, FileOutputStream
和FileChannel
是將資料寫入檔案的兩種常見方法.
在本教程中,我們將探討它們的功能並了解它們的差異。
2. FileOutputStream
FileOutputStream
是java.io
套件的一部分,是將二進位資料寫入檔案最簡單的方法之一。對於簡單的寫入操作來說,這是一個不錯的選擇,尤其是對於較小的檔案。它的簡單性使其易於用於基本的文件寫入任務。
下面的程式碼片段示範如何使用FileOutputStream
將位元組數組寫入檔案:
byte[] data = "This is some data to write".getBytes();
try (FileOutputStream outputStream = new FileOutputStream("output.txt")) {
outputStream.write(data);
} catch (IOException e) {
// ...
}
在此範例中,我們首先建立一個包含要寫入的資料的位元組數組。接下來,我們初始化一個FileOutputStream
對象,並指定檔案名稱「 output.txt
」。 try-
with-resources 語句確保資源自動關閉。 FileOutputStream
的write()
方法將整個位元組數組「 data
」寫入檔案。
3. FileChannel
FileChannel
是java.nio.channels
套件的一部分,與FileOutputStream
相比,它提供了更進階、更靈活的檔案 I/O 操作。它特別適合處理較大的文件、隨機存取和效能關鍵型應用程式。它對緩衝區的使用允許更有效的資料傳輸和操作。
下面的程式碼片段示範如何使用FileChannel
將位元組數組寫入檔案:
byte[] data = "This is some data to write".getBytes();
ByteBuffer buffer = ByteBuffer.wrap(data);
try (FileChannel fileChannel = FileChannel.open(Path.of("output.txt"),
StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
fileChannel.write(buffer);
} catch (IOException e) {
// ...
}
在此範例中,我們建立一個ByteBuffer
並將位元組數組資料包裝到其中。然後我們使用FileChannel.open()
方法初始化一個FileChannel
物件。接下來,我們也指定檔案名稱「 output.txt
」和必要的開啟選項( StandardOpenOption.WRITE
和StandardOpenOption.CREATE
)。
然後FileChannel
的write()
方法將ByteBuffer
的內容寫入指定的檔案。
4. 資料存取
在本節中,我們將深入探討FileOutputStream
和FileChannel
在資料存取方面的差異。
4.1. FileOutputStream
FileOutputStream
按順序寫入數據,這意味著它按照給定的順序從頭到尾將位元組寫入檔案。它不支援跳到檔案中的特定位置來讀取或寫入資料。
以下是使用FileOutputStream
順序寫入資料的範例:
byte[] data1 = "This is the first line.\n".getBytes();
byte[] data2 = "This is the second line.\n".getBytes();
try (FileOutputStream outputStream = new FileOutputStream("output.txt")) {
outputStream.write(data1);
outputStream.write(data2);
} catch (IOException e) {
// ...
}
在此程式碼中,「 This is the first line.
首先寫的是「 This is the second line.
” 在「 output.txt
」檔案中的新行上。如果不從頭開始重寫所有內容,我們就無法在文件中間寫入資料。
4.2. FileChannel
另一方面, FileChannel
允許我們在檔案中的任何位置讀取或寫入資料。這是因為FileChannel
使用可以移動到檔案中任何位置的檔案指標。這是透過使用position()
方法來實現的,該方法設定檔案中下一次讀取或寫入發生的位置。
下面的程式碼片段示範了FileChannel
如何將資料寫入檔案中的特定位置:
ByteBuffer buffer1 = ByteBuffer.wrap(data1);
ByteBuffer buffer2 = ByteBuffer.wrap(data2);
try (FileChannel fileChannel = FileChannel.open(Path.of("output.txt"),
StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
fileChannel.write(buffer1);
fileChannel.position(10);
fileChannel.write(buffer2);
} catch (IOException e) {
// ...
}
在此範例中, data1
寫入檔案的開頭。現在,我們想要將data2
從位置10
開始插入到檔案中。因此,我們使用fileChannel.position(10)
將位置設為10
,然後從第 10 個位元組開始寫入data2
。
5. 並發和線程安全
在本節中,我們將探討FileOutputStream
和FileChannel
如何處理並發和執行緒安全性。
5.1. FileOutputStream
FileOutputStream
不處理內部同步。如果兩個執行緒嘗試同時寫入同一個FileOutputStream
,則結果可能是輸出檔案中出現不可預測的資料交錯。因此我們需要同步來確保線程安全。
以下是使用FileOutputStream
進行外部同步的範例:
final Object lock = new Object ();
void writeToFile(String fileName, byte[] data) {
synchronized (lock) {
try (FileOutputStream outputStream = new FileOutputStream(fileName, true)) {
outputStream.write(data);
log.info("Data written by " + Thread.currentThread().getName());
} catch (IOException e) {
// ...
}
}
}
在本例中,我們使用公共鎖定物件來同步對檔案的存取。當多執行緒順序向文件寫入資料時,保證了執行緒安全:
Thread thread1 = new Thread(() -> writeToFile("output.txt", data1));
Thread thread2 = new Thread(() -> writeToFile("output.txt", data2));
thread1.start();
thread2.start();
5.2. FileChannel
相較之下, FileChannel
支援檔案鎖定,允許我們鎖定特定的檔案部分,以防止其他執行緒或進程同時存取該資料。
以下是使用FileChannel
和FileLock
來處理並發存取的範例:
void writeToFileWithLock(String fileName, ByteBuffer buffer, int position) {
try (FileChannel fileChannel = FileChannel.open(Path.of(fileName), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
// Acquire an exclusive lock on the file
try (FileLock lock = fileChannel.lock(position, buffer.remaining(), false)) {
fileChannel.position(position);
fileChannel.write(buffer);
log.info("Data written by " + Thread.currentThread().getName() + " at position " + position);
} catch (IOException e) {
// ...
}
} catch (IOException e) {
// ...
}
}
在此範例中, FileLock
物件用於確保鎖定正在寫入的檔案部分,以防止其他執行緒同時存取它。當執行緒呼叫writeToFileWithLock()
時,它首先取得檔案特定部分的鎖:
Thread thread1 = new Thread(() -> writeToFileWithLock("output.txt", buffer1, 0));
Thread thread2 = new Thread(() -> writeToFileWithLock("output.txt", buffer2, 20));
thread1.start();
thread2.start();
6. 性能
在本節中,我們將使用 JMH 來比較FileOutputStream
和FileChannel
的效能。我們將創建一個基準測試類,其中包括FileOutputStream
和FileChannel
基準測試,以評估它們在處理大型檔案方面的效能:
@Setup
public void setup() {
largeData = new byte[1000 * 1024 * 1024]; // 1 GB of data
Arrays.fill(largeData, (byte) 1);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void testFileOutputStream() {
try (FileOutputStream outputStream = new FileOutputStream("largeOutputStream.txt")) {
outputStream.write(largeData);
} catch (IOException e) {
// ...
}
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public void testFileChannel() {
ByteBuffer buffer = ByteBuffer.wrap(largeData);
try (FileChannel fileChannel = FileChannel.open(Path.of("largeFileChannel.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
fileChannel.write(buffer);
} catch (IOException e) {
// ...
}
}
讓我們執行基準測試並比較FileOutputStream
和FileChannel
的效能。結果顯示每個操作所花費的平均時間(以毫秒為單位):
Options opt = new OptionsBuilder()
.include(FileIOBenchmark.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
運行基準測試後,我們得到了以下結果:
Benchmark Mode Cnt Score Error Units
FileIOBenchmark.testFileChannel avgt 5 431.414 ± 52.229 ms/op
FileIOBenchmark.testFileOutputStream avgt 5 556.102 ± 91.512 ms/op
FileOutputStream
設計宗旨是簡單易用。然而,當處理具有高頻 I/O 操作的大檔案時,它會引入一些開銷。這是因為FileOutputStream
操作是阻塞的,這意味著每個寫入操作必須在下一個寫入作業開始之前完成。
另一方面, FileChannel
支援記憶體映射I/O,可以將檔案的一部分對應到記憶體中。這使得可以直接在記憶體空間中進行資料操作,從而實現更快的傳輸。
七、結論
在本文中,我們探討了兩種檔案 I/O 方法: FileOutputStream
和FileChannel
。 FileOutputStream
為基本文件寫入任務提供了簡單性和易用性,非常適合較小的文件和順序資料寫入。
另一方面, FileChannel
提供了高級功能,例如直接緩衝區訪問,以提高大型檔案的效能。
與往常一樣,範例的原始程式碼可在 GitHub 上取得。