Java 檔案讀取全方位實作:從基礎到高效能
檔案讀取是 Java 開發最基礎也最常見的操作。從傳統 FileReader/BufferedReader 到 NIO.2 Files API、記憶體映射、串流處理大檔案,本文系統整理各種場景下的最佳實作模式、編碼處理、資源管理與效能優化。
核心 API 演進對比
| API 代數 | 代表類別 | 特點 | 適用場景 |
| IO 流 (JDK 1.0) | FileInputStream、FileReader、BufferedReader | 阻塞、裝飾者模式、逐行/位元組讀取 | 中小檔案、行導向文本、相容舊代碼 |
| NIO (JDK 1.4) | FileChannel、ByteBuffer、MappedByteBuffer | 通道/緩衝區、非阻塞、記憶體映射 | 大檔案、高效能、隨機存取 |
| NIO.2 (JDK 7+) | Files、Paths、Path | 靜態工具方法、串流式、函數式、異步 | 現代首選、簡潔、功能豐富 |
| Stream API (JDK 8+) | Files.lines()、BufferedReader.lines() | 惰性串流、函數式處理、自動關閉 | 文本逐行處理、過濾轉換、並行 |
現代首選:Files API 完整範例
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;
public class FileReadExamples {
public static void main(String[] args) throws IOException {
Path path = Paths.get("data", "sample.txt");
// 1. 一次性讀取全文 (小檔案)
String content = Files.readString(path, StandardCharsets.UTF_8); // JDK 11+
System.out.println("全文長度: " + content.length());
// 2. 讀取所有行到 List (中等檔案)
List lines = Files.readAllLines(path, StandardCharsets.UTF_8);
System.out.println("行數: " + lines.size());
// 3. 串流逐行處理 (大檔案、記憶體友好) - JDK 8+
try (Stream lineStream = Files.lines(path, StandardCharsets.UTF_8)) {
long errorCount = lineStream
.filter(line -> line.contains("ERROR"))
.peek(System.out::println)
.count();
System.out.println("錯誤行數: " + errorCount);
}
// 4. 並行處理 (多核加速,無序)
try (Stream lineStream = Files.lines(path, StandardCharsets.UTF_8)) {
Map wordFreq = lineStream
.parallel()
.flatMap(line -> Arrays.stream(line.split("\\s+")))
.filter(w -> !w.isEmpty())
.collect(Collectors.groupingBy(String::toLowerCase, Collectors.counting()));
System.out.println("高頻詞: " + wordFreq.entrySet().stream()
.sorted(Map.Entry.comparingByValue().reversed())
.limit(10).toList());
}
// 5. 二進位檔案讀取
byte[] bytes = Files.readAllBytes(path); // 小檔案
// 大檔案用 FileChannel + ByteBuffer 或 MappedByteBuffer
}
}
傳統 IO 經典模式 (仍廣泛使用)
// 緩衝讀取器模式 - 逐行處理
public static void readWithBufferedReader(Path path) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
// 處理每一行
processLine(line);
}
}
}
// 位元組輸入流 - 二進位檔案
public static byte[] readBytes(Path path) throws IOException {
try (InputStream in = Files.newInputStream(path)) {
return in.readAllBytes(); // JDK 9+
// 或手動緩衝讀取大檔案
}
}
大檔案高效能讀取策略
| 策略 | 實作 | 優點 | 缺點 |
| 記憶體映射 | FileChannel.map(READ_ONLY, 0, size) | OS 層面零拷貝、隨機存取、大檔案極快 | 受限於虛擬記憶體、32位系統 2GB 限制、關閉延遲 |
| 直接緩衝區 | ByteBuffer.allocateDirect() | 避免堆外/堆內拷貝、DMA 友好 | 分配/回收成本高、需手動管理 |
| 分塊串流 | Files.lines() / BufferedReader | 記憶體恆定、支援並行、背壓 | 僅適用行導向文本 |
| 非同步 IO | AsynchronousFileChannel + CompletionHandler | 真正非阻塞、高併發 | 程式碼複雜、錯誤處理難 |
記憶體映射範例 (超大檔案)
public static void readLargeFileMapped(Path path) throws IOException {
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
long fileSize = channel.size();
long position = 0;
final int MAP_SIZE = 64 * 1024 * 1024; // 64MB 每次映射
while (position < fileSize) {
long remain = fileSize - position;
long size = Math.min(MAP_SIZE, remain);
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, position, size);
// 處理 buffer 中的資料
processBuffer(buffer);
// 釋放映射 (JDK 9+ cleaner,或反射 hack)
if (buffer instanceof sun.nio.ch.DirectBuffer) {
((sun.nio.ch.DirectBuffer) buffer).cleaner().clean();
}
position += size;
}
}
}
編碼處理最佳實踐
- 永遠顯式指定編碼:
StandardCharsets.UTF_8、StandardCharsets.ISO_8859_1,避免平台預設編碼差異
- BOM 處理:UTF-8 BOM (EF BB BF) 需手動跳過,
Files.readString JDK 17+ 自動處理
- 編碼偵測:未知編碼用 juniversalchardet 或 encoding-detector
路徑處理跨平台
// 正確:使用 Paths / Path API
Path path = Paths.get("data", "logs", "app.log"); // 自動用系統分隔符
Path absolute = path.toAbsolutePath();
Path normalized = path.normalize(); // 處理 . ..
// 錯誤:硬編碼分隔符
// String bad = "data/logs/app.log"; // Windows 失敗
// String bad = "data\\logs\\app.log"; // Linux 失敗
常見異常處理
| 異常 | 原因 | 處理 |
| FileNotFoundException | 檔案不存在/是目錄/無權限 | 檢查 Files.exists()/isReadable() |
| IOException | 磁碟錯誤/網路中斷/管道破裂 | 重試、記錄、優雅降級 |
| MalformedInputException | 編碼不匹配 (含非法位元組序列) | 指定正確編碼、用 CharsetDecoder.REPLACE |
| OutOfMemoryError | 一次性讀取超大檔案 | 改用串流/分塊/映射讀取 |