Java IO NIO Path Files

[Java] 檔案 IO

Java 的 IO 有點考古意味——java.io 是初代、java.nio.file 是 Java 7 的重做。新程式碼一律用後者(Path + Files),但讀舊 codebase 還是會看到 File / FileInputStream。這篇以 NIO 為主,舊 API 只看到「認得就好」的程度。

三套 API 的關係

世代入口何時用
老 IOjava.io.FileFileInputStream維護舊程式才碰
NIO(Java 7+)java.nio.file.PathFiles新程式碼一律用這個
Stream IOjava.ioReader / 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);

不要再用 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.linesFiles.listFiles.walknewBufferedReader/WriternewInputStream/OutputStream 全都實作 AutoCloseable一律包在 try-with-resources 裡,否則檔案 handle 會洩漏。

暫存檔與資料夾

Path tmpFile = Files.createTempFile("prefix-", ".tmp");
Path tmpDir  = Files.createTempDirectory("prefix-");
tmpFile.toFile().deleteOnExit();    // JVM 結束時刪

監看檔案變更:WatchService

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()) {
        System.out.println(e.kind() + " " + e.context());
    }
    key.reset();
}

範例裡的 while (true) 實務上要有結束條件——常見做法是捕捉外部 signal(例如 AtomicBoolean running 配 shutdown hook),或計數到達後 break。要停掉監看,WatchKey.cancel() 取消特定路徑、WatchService.close() 關整個服務。

寫熱重載、檔案同步工具會用到這個。

老 API:java.io.File(維護用看得懂就好)

File f = new File("a.txt");
f.exists();
f.length();
f.delete();
new FileInputStream(f);
new FileWriter(f);

舊程式才會看到。新程式碼一律用 Path + Files

Resource 讀取(jar 內檔案)

jar 裡的資源不是檔案系統上的檔案,不能用 Path,要走 classpath:

try (InputStream in =
        getClass().getResourceAsStream("/config.properties")) {
    Properties props = new Properties();
    props.load(in);
}

/ 開頭表示從 classpath 根開始找。

編碼一律寫 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 範例的全貌才會浮出來。

Latest Updates

  • 2026.06.11 Content updated