Java 的 IO 有點考古意味——java.io 是初代、java.nio.file 是 Java 7 的重做。檔案路徑與常用操作優先用 Path + Files;InputStream、Reader 等 java.io 類型仍是現行 API,不是整套被淘汰。這篇以 NIO 為主,舊 API 只看到「認得就好」的程度。
三套 API 的關係
| 世代 | 入口 | 何時用 |
|---|---|---|
| 傳統 IO | java.io.File、FileInputStream | 舊介面整合,或需要 stream 的場合 |
| NIO.2(Java 7+) | java.nio.file.Path、Files | 檔案路徑與常用操作優先考慮 |
| Stream IO | java.io 的 Reader / Writer / InputStream | 跟 NIO 互通,需要逐 byte/char 處理時 |
整篇以 NIO 為主。
Path:路徑物件
import java.nio.file.*;
Path p = Path.of("data", "users", "jeremy.json");
// 等同 Path.of("data/users/jeremy.json"),跨平台自動處理分隔符
p.toString(); // "data\users\jeremy.json"(Windows)
p.getFileName(); // jeremy.json
p.getParent(); // data\users
p.getRoot(); // null(相對路徑)
p.toAbsolutePath();
p.normalize(); // 解析掉 . 與 ..
p.resolve("backup.json"); // 接子路徑
p.relativize(otherPath);
Path.of 是 Java 11+;Java 7/8 可用 Paths.get。normalize() 只整理字面路徑,不解析符號連結或驗證存在;真實路徑要看 toRealPath()。範例省略 java.io、java.util、Stream 與 StandardCharsets 等對應 import,src/dst 代表事先準備的 Path。
不要再用 new File("a/b") 加手拼 / 或 \ 了,這些 Path 全包。
Files:操作檔案系統
存在性與屬性
Files.exists(p);
Files.isRegularFile(p);
Files.isDirectory(p);
Files.size(p); // 位元組
Files.getLastModifiedTime(p);
建立與刪除
Files.createDirectories(Path.of("logs/2026")); // 整條建出來
Files.createFile(Path.of("a.txt")); // 已存在會丟 FileAlreadyExistsException
Files.delete(p); // 不存在會丟
Files.deleteIfExists(p); // 不存在不丟
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
讀寫文字檔
一次讀全部
String s = Files.readString(p); // Java 11+,預設 UTF-8
String s2 = Files.readString(p, StandardCharsets.UTF_8); // 顯式編碼
List<String> lines = Files.readAllLines(p);
一次寫全部
Files.writeString(p, "hello");
Files.writeString(p, "added\n", StandardOpenOption.APPEND);
Files.write(p, List.of("a", "b", "c"));
大檔案:用 stream 逐行讀
readAllLines 會把整個檔載進記憶體,大檔不適合。改用 Files.lines(搭配 try-with-resources):
try (Stream<String> lines = Files.lines(p)) {
lines.filter(s -> s.startsWith("ERROR"))
.forEach(System.out::println);
}
Files.lines 回傳的 Stream 持有檔案 handle,一定要包 try-with-resources,否則 handle 會洩漏。
BufferedReader / BufferedWriter
需要更細的控制時,改拿 buffered reader / writer 自己處理:
try (BufferedReader r = Files.newBufferedReader(p)) {
String line;
while ((line = r.readLine()) != null) {
...
}
}
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("hello");
w.newLine();
}
二進位檔
byte[] bytes = Files.readAllBytes(p);
Files.write(p, bytes);
// 大檔用 stream
try (InputStream in = Files.newInputStream(p);
OutputStream out = Files.newOutputStream(dst)) {
in.transferTo(out); // Java 9+,一次搬完
}
列出資料夾內容
非遞迴
try (Stream<Path> children = Files.list(Path.of("data"))) {
children.forEach(System.out::println);
}
遞迴 walk
try (Stream<Path> all = Files.walk(Path.of("src"))) {
all.filter(Files::isRegularFile)
.filter(f -> f.toString().endsWith(".java"))
.forEach(System.out::println);
}
用 glob 找
try (DirectoryStream<Path> ds =
Files.newDirectoryStream(Path.of("data"), "*.json")) {
for (Path p : ds) {
...
}
}
try-with-resources 是必須
Files.lines、Files.list、Files.walk、newBufferedReader/Writer、newInputStream/OutputStream 全都實作 AutoCloseable,一律包在 try-with-resources 裡,否則檔案 handle 會洩漏。
暫存檔與資料夾
Path tmpFile = Files.createTempFile("prefix-", ".tmp");
Path tmpDir = Files.createTempDirectory("prefix-");
tmpFile.toFile().deleteOnExit(); // JVM 結束時刪
監看檔案變更:WatchService
try (WatchService ws = FileSystems.getDefault().newWatchService()) {
Path.of("data").register(ws,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
while (true) {
WatchKey key = ws.take(); // 阻塞等事件
for (WatchEvent<?> e : key.pollEvents()) {
if (e.kind() == StandardWatchEventKinds.OVERFLOW) {
// 事件可能遺失;實務上重新掃描檔案狀態
continue;
}
System.out.println(e.kind() + " " + e.context());
}
if (!key.reset()) break; // 路徑已失效
}
}
這段放在宣告或處理 IOException/InterruptedException 的方法中。外部停止時,可中斷等待執行緒,或關閉服務並處理 ClosedWatchServiceException;單改 boolean 不會喚醒阻塞中的 take()。WatchKey.cancel() 取消特定路徑,也不能保證喚醒等待者。
寫熱重載、檔案同步工具會用到這個。
老 API:java.io.File(維護用看得懂就好)
File f = new File("a.txt");
f.exists();
f.length();
f.delete();
new FileInputStream(f);
new FileWriter(f);
需要跟既有 File 介面整合時仍可用;預設檔案系統中可用 toPath()/toFile() 互轉。單純操作路徑時優先用 Path + Files。
Resource 讀取(jar 內檔案)
jar 裡的 classpath 資源不能直接當成一般檔案路徑讀取,通常走 classpath stream:
try (InputStream in =
getClass().getResourceAsStream("/config.properties")) {
Properties props = new Properties();
if (in == null) throw new FileNotFoundException("config.properties");
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
}
/ 開頭表示從 classpath 根開始找。這裡明確使用 UTF-8;Properties.load(InputStream) 固定按 ISO-8859-1 解讀,不會跟著 Java 18 的預設編碼變成 UTF-8。
編碼一律寫 UTF-8
Java 18+ 的預設編碼已經改成 UTF-8(JEP 400)。但要顧跨版本、跨平台相容,顯式寫 StandardCharsets.UTF_8 永遠不會錯:
Files.readString(p, StandardCharsets.UTF_8);
Files.newBufferedWriter(p, StandardCharsets.UTF_8);
你可能注意到 Files.lines / Files.walk 回傳的是 Stream<T>,不是普通迴圈能直接吃的東西——下一篇展開 Java 8 的 Lambda 與 Stream API,這些 IO 範例的全貌才會浮出來。
參考文件
更新紀錄
- 2026.11.05 內容更新
- 2026.06.11 內容更新

