優雅的操作檔案:java.nio.file 庫介紹

2023-05-10 06:00:39

概述

在早期的 Java 版本中,檔案 IO 操作功能一直相對較弱,主要存在以下問題:

  1. 缺乏對現代檔案系統的支援:只提供的基礎的檔案操作,不支援很多現代的檔案系統
  2. API 不夠直觀:檔案操作的 API 設計相對較為複雜和冗長,使用體驗感很差
  3. 對於大檔案處理和並行效能不夠:簡單的 I/O 模型,沒有充分利用現代硬體的效能優勢,而且還有很多同步的問題

但 Java 在後期版本中引入了 java.nio.file 庫來提高 Java 對檔案操作的能力。還增加的流的功能,似乎使得檔案變成更好用了。所以本章,我們就來主要介紹 java.nio.file 中常用的類和模組,大致如下:

  1. Path 路徑:Paths 模組和 Path 工具類介紹
  2. Files 檔案:File 和 FileSystems 工具類介紹
  3. 檔案管理服務:WatchService 、PathMatcher 等等檔案服務

Path 路徑

java.nio.file.Pathsjava.nio.file.Path 類在 Java NIO 檔案 I/O 框架中用於處理檔案系統路徑。以下是對它們的簡單介紹:

  • Paths 模組:Paths 模組提供了一些靜態方法來建立 Path 物件,Path 物件表示檔案系統中的路徑。例如,可以使用 Paths.get() 方法建立一個 Path 物件,這個物件表示一個檔案路徑。
  • Path 類:Path 類代表一個檔案系統中的路徑,它提供了一系列的方法來操作檔案路徑。例如,可以使用 Path.toAbsolutePath() 方法獲取一個絕對路徑,或者使用 Path.getParent() 方法獲取路徑的父路徑。

關於跨平臺:Path 物件可以工作在不同作業系統的不同檔案系統之上,它幫我們遮蔽了作業系統之間的差異

以下是一些簡單使用場景範例:

import java.nio.file.Path;
import java.nio.file.Paths;

public class PathExample {

    public static void main(String[] args) {
        // 建立一個絕對路徑
        Path absolutePath = Paths.get("C:\\Users\\phoenix\\file.txt");     // 這裡傳入 "example\\file.txt" 建立的相對路徑
        System.out.println("Absolute path: " + absolutePath);
        // 獲取父路徑
        System.out.println("Parent path: " + absolutePath.getParent());
        // 獲取檔名
        System.out.println("File name: " + absolutePath.getFileName());
        // 獲取根路徑
        System.out.println("Root path: " + absolutePath.getRoot());
        // 合併路徑
        Path resolvePath = Paths.get("C:\\Users\\phoenix").resolve("file.txt");
        System.out.println("Merged path:" + resolvePath);
    }
}

輸出結果:

Absolute path: C:\Users\phoenix\file.txt
Parent path: C:\Users\phoenix
File name: file.txt
Root path: C:\
Merged path:C:\Users\phoenix\file.txt

從這裡你不僅可以看出關於 PathsPath 類對於檔案路徑的一些操作方法的使用,還能看得出我使用的是 Windows 作業系統。還有更多的用法可以檢視官方的 API 檔案,這裡就不過多贅述了。

Files 檔案

java.nio.file.Files 類是 Java NIO 檔案包中的一個實用工具類,它提供了一系列靜態方法,可以讓你方便地執行檔案系統中的各種操作,例如檔案的建立、刪除、複製、移動、讀取和寫入等。例如,可以使用 Files.exists() 方法檢查一個檔案是否存在,或者使用 Files.createDirectory() 方法建立一個新目錄。

以下是一些簡單使用場景範例:

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Arrays;
import java.util.List;

public class PathExample {

    public static void main(String[] args) throws IOException {
        Path path = Paths.get("example.txt");
        // 1:檢查檔案是否存在
        boolean exists = Files.exists(path);
        System.out.println("File exists: " + exists);
        if (!exists) {
            // 2:不存在則建立檔案
            Files.createFile(path);
        }
        // 3:複製一個檔案
        Path target = Paths.get("example2.txt");
        Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
        // 4:建立目錄
        Path newDirectory = Paths.get("example");
        Files.createDirectories(newDirectory);
        // 4:移動檔案:將 example2.txt 移動到 example 目錄下
        Files.move(target, newDirectory.resolve("example2.txt"), StandardCopyOption.REPLACE_EXISTING);
        // 5:刪除檔案和目錄
        Files.delete(newDirectory.resolve("example2.txt"));
        Files.delete(newDirectory);				// 只能刪除空目錄
        // 6:將位元組陣列寫入檔案
        Files.write(path, "Hello World".getBytes());
        // 7:將文字行序列寫入檔案
        List<String> lines = Arrays.asList("Line 1", "Line 2", "Line 3");
        Files.write(path, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE);
        // 8:讀取檔案,並且列印所有行
        Files.readAllLines(path, StandardCharsets.UTF_8).forEach(System.out::println);
    }
}

輸出結果:

File exists: true
Line 1
Line 2
Line 3

也可以在專案根目錄下檢視檔案:

image-20230508234504230

以上程式碼範例展示瞭如何使用 Files 類進行常見的檔案操作。在實際專案中,您可以根據需要組合使用這些方法來滿足您的需求。

補充:

Files.delete 函數只能刪除空目錄,這個設計是有意為之的,因為遞迴地刪除檔案和目錄可能是一個非常危險的操作,尤其是當您不小心刪除了一個包含重要資料的目錄時。如果您想刪除一個包含子目錄和檔案的目錄,您需要先遞迴地刪除目錄中的所有子目錄和檔案,然後再刪除目錄本身。可以藉助 Files.walkFileTree 遍歷檔案目錄,然後呼叫 Files.delete 即可。

FileSystems 檔案系統

FileSystems 類提供了一組靜態方法來存取和操作預設檔案系統(通常是作業系統的本地檔案系統)以及其他檔案系統實現。以下是一個簡單的範例:

public class FileSystemsExample {

    public static void main(String[] args) {
        // 獲取預設檔案系統
        FileSystem fileSystem = FileSystems.getDefault();
        // 獲取檔案系統的路徑分隔符
        String pathSeparator = fileSystem.getSeparator();
        System.out.println("Path separator: " + pathSeparator);
        // 獲取檔案系統的根目錄
        for (Path root : fileSystem.getRootDirectories()) {
            System.out.println("Root directory: " + root);
        }
        // 使用檔案系統建立一個 path 路徑物件
        Path path = fileSystem.getPath("path", "to", "file.txt");
        System.out.println(path);
        // 是否唯讀
        System.out.println("is read only ?: " + fileSystem.isReadOnly());
        // 檔案系統的提供者
        System.out.println("provider: " + fileSystem.provider());
    }
}

輸出結果:

Path separator: \
Root directory: C:\
path\to\file.txt
is read only ?: false
provider: sun.nio.fs.WindowsFileSystemProvider@5b480cf9

FileSystem 工具類的方法並不多,可以參考它的 API,但通過 FileSystem 可以建立 WatchService 和 PathMatcher 子類

WatchService 檔案監控

WatchService 是一個檔案系統觀察者,基於 FileSystem 建立,主要用於監控檔案系統事件(如建立、修改、刪除檔案或目錄)。它可以幫助我們實時地檢測和處理檔案系統中的變化。如果你的業務中有需要監控檔案變化的場景,你可能會需要用到它,例如:

  • 檔案上傳
  • 實時備份
  • 熱載入設定

以下是一個簡單的範例:

import java.io.IOException;
import java.nio.file.*;

public class WatchServiceExample {

    public static void main(String[] args) throws IOException, InterruptedException {
        // 建立 WatchService
        WatchService watchService = FileSystems.getDefault().newWatchService();

        // 註冊監聽指定的目錄
        Path dir = Paths.get("C:\\Users\\phoenix");
        dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);

        while (true) {
            // 獲取並處理事件
            WatchKey key = watchService.take();
            for (WatchEvent<?> event : key.pollEvents()) {
                System.out.println("Event: " + event.kind() + " - " + event.context());
            }

            // 重置 key,繼續監聽
            if (!key.reset()) {
                break;
            }
        }
        watchService.close();
    }
}

啟動以上程式,程式就會監控我當前系統的使用者目錄,當我在使用者目錄建立檔案並且編輯,刪除,程式會輸出以下內容:

Event: ENTRY_CREATE - 新建 文字檔案.txt
Event: ENTRY_DELETE - 新建 文字檔案.txt
Event: ENTRY_CREATE - helloWorld.txt
Event: ENTRY_MODIFY - helloWorld.txt
Event: ENTRY_MODIFY - helloWorld.txt
Event: ENTRY_MODIFY - helloWorld.txt
Event: ENTRY_DELETE - helloWorld.txt

PathMatcher 檔案匹配

PathMatcher 是一個檔案路徑匹配介面,它可以幫助我們在遍歷檔案系統時,根據特定規則過濾出符合條件的檔案或目錄。它可以使用多種匹配語法(如 glob 和 regex),使得處理檔名或目錄名的模式變得更加靈活和高效。PathMatcher 的使用場景包括:

  • 檔案過濾:在搜尋檔案時,我們可能需要根據檔名或目錄名的模式來過濾結果
  • 批次操作:當我們需要對檔案系統中的一組檔案或目錄執行批次操作時,PathMatcher 可以幫助我們找到符合特定規則的檔案或目錄
  • 目錄監控:可以結合 WatchService 對目錄監控,然後通過 PathMatcher 過濾找出我們想要檔案,如:.log 檔案的建立,修改等

以下是一個簡單範例程式碼:

import java.io.IOException;
import java.nio.file.*;
import java.util.stream.Stream;

public class PathMatcherExample {

    public static void main(String[] args) throws IOException {
        // 建立 PathMatcher,使用 glob 語法:匹配所有以 .tmp 結尾的檔案(臨時檔案)
        FileSystem fileSystem = FileSystems.getDefault();
        PathMatcher matcher = fileSystem.getPathMatcher("glob:*.tmp");
        // 在指定目錄,找到匹配的檔案,然後進行刪除
        try (Stream<Path> walk = Files.walk(Paths.get("path/to/directory"))) {
            walk.filter(path -> matcher.matches(path.getFileName())).forEach(path -> {
                System.out.println(path.getFileName());
                try {
                    Files.delete(path);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }
}

上面的範例程式是通過 PathMatcher 匹配 .tmp 結尾的臨時檔案,然後進行刪除的範例,結合 PathMatcher 可以輕鬆的完成一個清理臨時檔案的小程式。

讀檔案內容

上面的範例都是操作檔案和目錄,這裡介紹一下如何讀檔案的內容,為了方便演示讀取檔案,先在 path/to/file.txt 相對目錄下建立一個範例文字:

Java is a high-level programming language.
Python is an interpreted, high-level programming language.
JavaScript is a scripting language for Web development.
C++ is a general-purpose programming language.
Rust is a systems programming language.

讀檔案主要用到 Files 類的兩個方法:

  1. readAllLines() 方法:一次性載入,主要用於讀取小到中等的檔案
  2. lines() 方法:逐行讀取,適用於大檔案

小檔案

readAllLines() 適用於讀取小到中等大小的檔案,因為它會將整個檔案內容載入到記憶體中,這個方法適用於在讀取檔案內容後立即處理整個檔案的情況。使用範例:

public class LinesExample {

    public static void main(String[] args) throws IOException {
        // 讀取全部檔案
        List<String> lines = Files.readAllLines(Paths.get("path/to/file.txt"), StandardCharsets.UTF_8);

        // 對檔案內容進行處理
        Map<String, Long> wordFrequency = lines.stream()
                .flatMap(line -> Arrays.stream(line.split("\\s+")))
                .map(String::toLowerCase)
                .collect(Collectors.groupingBy(word -> word, Collectors.counting()));

        System.out.println("Word Frequency:");
        wordFrequency.forEach((word, count) -> System.out.printf("%s: %d%n", word, count));
    }
}

大檔案

lines() 方法: 使用場景:適用於讀取大型檔案,因為它不會一次性將整個檔案內容載入到記憶體中。通過使用 Java 8 的 Stream API,可以在讀取檔案內容時同時處理每一行,從而提高處理效率。使用範例:

public class LinesExample {

    public static void main(String[] args) throws IOException {
        Path filePath = Paths.get("path/to/file.txt");

        // 逐行讀取,並且在內容進行處理
        Stream<String> lines = Files.lines(filePath);
        Map<String, Long> wordFrequency = lines
                .skip(3)            // 跳過前 3 行
                .flatMap(line -> Arrays.stream(line.split("\\s+")))
                .map(String::toLowerCase)
                .collect(Collectors.groupingBy(word -> word, Collectors.counting()));

        System.out.println("Word Frequency:");
        wordFrequency.forEach((word, count) -> System.out.printf("%s: %d%n", word, count));
        lines.close();
    }
}

輸出結果:

Word Frequency:
rust: 1
a: 2
c++: 1
systems: 1
language.: 2
is: 2
programming: 2
general-purpose: 1

總結

在過去,java.io 包主要負責處理檔案 I/O。但是它存在一些問題,例如效能不佳、API 不直觀、檔案後設資料操作困難等。為了解決這些問題,後期的 Java 版本引入了新的 java.nio.file 庫。現在 java.nio.file 已經成為處理檔案 I/O 的首選庫。 PathFilesFileSystem 等工具類,可以更方便快捷的存取和操作檔案系統。目前大多數的開發人員普遍認為 java.nio.file 比傳統的 java.io 包更直觀且易於使用。雖然 java.nio.file 庫已經非常成熟,但是隨著作業系統和檔案系統的發展,我們仍然可以期待在未來的 Java 版本中看到它的一些擴充套件和改進。