概述
在早期的 Java 版本中,文件 IO 操作功能一直相對較弱,主要存在以下問題:
- 缺乏對現代文件系統的支持:只提供的基礎的文件操作,不支持很多現代的文件系統
- API 不夠直觀:文件操作的 API 設計相對較爲複雜和冗長,使用體驗感很差
- 對於大文件處理和併發性能不夠:簡單的 I/O 模型,沒有充分利用現代硬件的性能優勢,而且還有很多同步的問題
但 Java 在後期版本中引入了 java.nio.file
庫來提高 Java 對文件操作的能力。還增加的流的功能,似乎使得文件變成更好用了。所以本章,我們就來主要介紹 java.nio.file
中常用的類和模塊,大致如下:
- Path 路徑:Paths 模塊和 Path 工具類介紹
- Files 文件:File 和 FileSystems 工具類介紹
- 文件管理服務:WatchService 、PathMatcher 等等文件服務
Path 路徑
java.nio.file.Paths
和 java.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
從這裏你不僅可以看出關於 Paths
和 Path
類對於文件路徑的一些操作方法的使用,還能看得出我使用的是 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
也可以在項目根目錄下查看文件:
以上代碼示例展示瞭如何使用 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 類的兩個方法:
readAllLines()
方法:一次性加載,主要用於讀取小到中等的文件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 的首選庫。 Path
、Files
、FileSystem
等工具類,可以更方便快捷的訪問和操作文件系統。目前大多數的開發人員普遍認爲 java.nio.file
比傳統的 java.io
包更直觀且易於使用。雖然 java.nio.file
庫已經非常成熟,但是隨着操作系統和文件系統的發展,我們仍然可以期待在未來的 Java 版本中看到它的一些擴展和改進。