返回首頁

[Java] 檔案 IO

Java 的 IO 有點考古意味——java.io 是初代、java.nio.file 是 Java 7 的重做。檔案路徑與常用操作優先用 Path + Files;InputStream、Reader 等 java.io 類型仍是現行 API,不是整套被淘汰。這篇以 NIO 為主,舊 API 只看到「認得就好」的程度。

三套 API 的關係

世代入口何時用
傳統 IOjava.io.File、FileInputStream舊介面整合,或需要 stream 的場合
NIO.2(Java 7+)java.nio.file.Path、Files檔案路徑與常用操作優先考慮
Stream IOjava.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 內容更新