核心 API 概覽 (java.io / java.nio.file)
| 場景 | 傳統 IO (java.io) | 現代 NIO.2 (java.nio.file) |
| 文件/目錄元數據 | File | Path, Files, BasicFileAttributes |
| 文本讀寫 | FileReader/Writer, BufferedReader/Writer | Files.readAllLines, Files.newBufferedReader, Files.write |
| 二進制讀寫 | FileInputStream/OutputStream, BufferedInputStream/OutputStream | Files.newInputStream, Files.newOutputStream, Files.readAllBytes |
| 隨機訪問 | RandomAccessFile | SeekableByteChannel |
| 目錄遍歷 | File.listFiles() | Files.walk, Files.walkFileTree, DirectoryStream |
1. 文件/目錄建立、檢查、刪除
// NIO.2 風格 (推薦)
Path file = Paths.get("/tmp/demo.txt");
Path dir = Paths.get("/tmp/demo-dir");
Files.createDirectories(dir); // 遞歸建立目錄
Files.createFile(file); // 建立文件 (存在則拋異常)
Files.deleteIfExists(file); // 存在才刪除,不存在不報錯
Files.deleteIfExists(dir); // 目錄必須為空
// 傳統 File 風格
File f = new File("/tmp/demo.txt");
f.getParentFile().mkdirs(); // 確保父目錄存在
f.createNewFile(); // 返回 boolean
f.delete(); // 返回 boolean
2. 文件屬性查詢
Path path = Paths.get("/tmp/demo.txt");
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
System.out.println("是否文件: " + attrs.isRegularFile());
System.out.println("是否目錄: " + attrs.isDirectory());
System.out.println("大小: " + attrs.size() + " bytes");
System.out.println("創建時間: " + attrs.creationTime());
System.out.println("最後修改: " + attrs.lastModifiedTime());
System.out.println("最後訪問: " + attrs.lastAccessTime());
// 權限檢查
System.out.println("可讀: " + Files.isReadable(path));
System.out.println("可寫: " + Files.isWritable(path));
System.out.println("可執行: " + Files.isExecutable(path));
System.out.println("隱藏: " + Files.isHidden(path));
3. 文本讀取:逐行處理 (大文件首選)
// NIO.2 + try-with-resources (自動關閉)
Path path = Paths.get("/var/log/app.log");
try (Stream lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.filter(line -> line.contains("ERROR"))
.forEach(System.out::println);
}
// 傳統 BufferedReader
try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
// 處理每行
}
}
4. 文本寫入:覆蓋 / 追加 / 帶換行
Path path = Paths.get("/tmp/output.txt");
List lines = List.of("第一行", "第二行", "第三行");
// 覆蓋寫入 (自動換行)
Files.write(path, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
// 追加寫入
Files.write(path, List.of("追加行"), StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
// BufferedWriter 精細控制
try (BufferedWriter bw = Files.newBufferedWriter(path, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) {
bw.write("手動控制");
bw.newLine(); // 跨平台換行
bw.flush(); // 強制刷新到磁盤
}
5. 二進制讀寫
// 讀取全部位元組 (小文件)
byte[] data = Files.readAllBytes(Paths.get("/tmp/image.png"));
// 流式寫入 (大文件)
try (InputStream in = Files.newInputStream(Paths.get("/big/in.dat"));
OutputStream out = Files.newOutputStream(Paths.get("/big/out.dat"),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
6. 目錄遍歷三種方式
Path dir = Paths.get("/project/src");
// 1. Files.walk (Stream, 深度優先, 可限制深度)
try (Stream stream = Files.walk(dir, 3, FileVisitOption.FOLLOW_LINKS)) {
stream.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// 2. Files.walkFileTree (Visitor 模式, 完全控制)
Files.walkFileTree(dir, new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".class")) {
System.out.println("編譯產物: " + file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
log.warn("無法訪問: " + file, exc);
return FileVisitResult.CONTINUE;
}
});
// 3. DirectoryStream (僅當前目錄, 低記憶體)
try (DirectoryStream stream = Files.newDirectoryStream(dir, "*.java")) {
for (Path entry : stream) System.out.println(entry);
}
7. 隨機訪問:RandomAccessFile / SeekableByteChannel
// 隨機讀寫特定位置
try (RandomAccessFile raf = new RandomAccessFile("/tmp/data.bin", "rw")) {
raf.seek(100); // 跳到第 100 字節
raf.writeInt(0xCAFEBABE); // 寫入 int
raf.seek(100);
int val = raf.readInt(); // 讀回驗證
}
// NIO.2 通道版
try (SeekableByteChannel ch = Files.newByteChannel(
Paths.get("/tmp/data.bin"),
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
ch.position(100);
ByteBuffer buf = ByteBuffer.allocate(4).putInt(0xCAFEBABE);
buf.flip();
ch.write(buf);
}
8. 判斷空文件 / 讀取到文件末尾
// NIO.2
boolean empty = Files.size(path) == 0;
// 傳統
try (FileReader fr = new FileReader(path)) {
boolean empty = fr.read() == -1; // 首次讀取即返回 -1 表示空
}
9. 編碼處理鐵律
- 顯式指定 Charset:
StandardCharsets.UTF_8,絕不依賴平台默認編碼
- 讀取未知編碼文件:嘗試 UTF-8 → 失敗嘗試 GBK/ISO-8859-1,或用
juniversalchardet 自動檢測
10. 常見坑與最佳實踐
| 坑點 | 正確姿勢 |
| 相對路徑依賴工作目錄 | 用 Path.of("").toAbsolutePath() 或配置絕對路徑 |
大文件 readAllBytes/readAllLines 導致 OOM | 流式處理 Files.lines()/InputStream |
| 忘記關閉流/通道 | 全程 try-with-resources |
| Windows 路徑分隔符 \ 轉義問題 | 用 Paths.get("C:/data", "file.txt") 或 Path.of("C:", "data", "file.txt") |
| 併發寫入同一文件數據競爭 | 文件鎖 FileChannel.lock()、寫臨時文件再 Files.move(ATOMIC_MOVE) |
NIO.2 (JDK 7+) 是現代 Java 文件操作標準:API 更清晰、異常更豐富、支持符號鏈接、屬性讀取、Stream 化處理。新代碼請優先選用 Path/Files。