java.io.File 與 java.io.FileDescriptor 是 Java 文件系統交互的兩大核心類。前者是路徑抽象,後者是底層文件描述符句柄。本文從 API 全景、源碼機制、實戰模式、標準流重定向、常見坑點五個維度深度解析。
核心定位:
File代表路徑名 (文件/目錄),不包含文件內容,不保證路徑存在。實現Serializable、Comparable<File>。
// 1. 字符串路徑 (相對/絕對)
File f1 = new File("data.txt"); // 相對路徑 (user.dir)
File f2 = new File("/home/user/data.txt"); // 絕對路徑 (Unix)
File f3 = new File("D:/data/data.txt"); // 絕對路徑 (Windows)
// 2. 父路徑 + 子路徑 (推薦,跨平台安全)
File dir = new File("data");
File f4 = new File(dir, "sub.txt"); // data/sub.txt
File f5 = new File("data", "sub.txt"); // 同上
// 3. URI 轉換 (支援 file:// 協議)
URI uri = new URI("file:/home/user/data.txt");
File f6 = new File(uri);
| 方法 | 說明 | 返回值 | 注意 |
|---|---|---|---|
mkdir() | 創建單級目錄 | boolean | 父目錄必須存在 |
mkdirs() | 創建多級目錄 | boolean | 遞歸創建父目錄 |
list() | 列出文件名 | String[] | 不含路徑,可為 null |
listFiles() | 列出 File 對象 | File[] | 可過濾,不遞歸子目錄 |
listFiles(FileFilter) | 過濾列出 | File[] | 如 f -> f.isDirectory() |
delete() | 刪除 (空目錄/文件) | boolean | 非空目錄失敗 |
deleteOnExit() | JVM 退出時刪除 | void | 臨時文件常用 |
// 創建文件 (原子操作)
File file = new File("data.txt");
boolean created = file.createNewFile(); // 存在返回 false
// 屬性查詢
System.out.println("存在: " + file.exists());
System.out.println("是文件: " + file.isFile());
System.out.println("是目錄: " + file.isDirectory());
System.out.println("可讀: " + file.canRead());
System.out.println("可寫: " + file.canWrite());
System.out.println("可執行: " + file.canExecute());
System.out.println("絕對路徑: " + file.getAbsolutePath());
System.out.println("規範路徑: " + file.getCanonicalPath()); // 解析 . .. 符號鏈接
System.out.println("大小: " + file.length() + " bytes");
System.out.println("最後修改: " + new Date(file.lastModified()));
System.out.println("父目錄: " + file.getParent());
System.out.println("文件名: " + file.getName());
// 安全創建臨時文件 (避免競態條件)
File tempFile = File.createTempFile("prefix-", ".tmp"); // 默認系統臨時目錄
tempFile.deleteOnExit(); // JVM 退出自動清理
// 指定目錄
File tempDir = new File("/custom/tmp");
File tempFile2 = File.createTempFile("app-", ".log", tempDir);
// 遞歸遍歷 (舊 API,慎用於深層目錄)
void listRecursive(File dir) {
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
if (f.isDirectory()) listRecursive(f);
else System.out.println(f.getAbsolutePath());
}
}
}
// 現代 NIO.2 (推薦:支援深度限制、並行流、異常處理)
try (Stream stream = Files.walk(Paths.get("data"), 3,
FileVisitOption.FOLLOW_LINKS)) {
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
本質:封裝作業系統文件描述符 (整數
fd) 的最終引用。FileInputStream/OutputStream、Socket內部持有FileDescriptor進行實際 IO 操作。
// 定義在 FileDescriptor.java
public static final FileDescriptor in = new FileDescriptor(0); // 標準輸入 (鍵盤)
public static final FileDescriptor out = new FileDescriptor(1); // 標準輸出 (屏幕)
public static final FileDescriptor err = new FileDescriptor(2); // 標準錯誤 (屏幕)
// 直接向標準輸出寫入字節
try (FileOutputStream fos = new FileOutputStream(FileDescriptor.out)) {
fos.write("Hello FileDescriptor.out\n".getBytes(StandardCharsets.UTF_8));
}
// 直接從標準輸入讀取
try (FileInputStream fis = new FileInputStream(FileDescriptor.in)) {
byte[] buf = new byte[1024];
int len = fis.read(buf);
System.out.println("讀取: " + new String(buf, 0, len, StandardCharsets.UTF_8));
}
File file = new File("shared.dat");
// 流 1: 寫入
try (FileOutputStream fos1 = new FileOutputStream(file)) {
FileDescriptor fd = fos1.getFD(); // 獲取底層描述符
fos1.write("First ".getBytes());
// 流 2: 共用同一描述符 (追加寫入)
try (FileOutputStream fos2 = new FileOutputStream(fd)) {
fos2.write("Second\n".getBytes());
}
// fos1 關閉時會關閉 fd,導致 fos2 失效!生產環境慎用共享 fd。
}
關鍵警示:
FileOutputStream(fd)共享描述符時,任一流關閉會關閉底層 fd,導致其他流失效。生產環境避免共享 fd,改用獨立流或RandomAccessFile共享文件鎖。
FileDescriptor fd = fos.getFD();
System.out.println("Valid: " + fd.valid()); // true/false
// valid() 檢查 fd 是否仍指向打開的文件/設備
PrintStream originalOut = System.out;
try (FileOutputStream fos = new FileOutputStream("system_out.log");
PrintStream ps = new PrintStream(fos, true, StandardCharsets.UTF_8)) {
System.setOut(ps); // 全局重定向
System.out.println("這行會寫入文件");
System.err.println("錯誤輸出仍在控制台");
} finally {
System.setOut(originalOut); // 恢復
}
ProcessBuilder pb = new ProcessBuilder("java", "-version");
pb.redirectOutput(ProcessBuilder.Redirect.to(new File("java_version.log")));
pb.redirectError(ProcessBuilder.Redirect.to(new File("java_error.log")));
Process process = pb.start();
process.waitFor();
// 日誌已寫入文件
// 線程間管道通訊
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
// 生產者線程
new Thread(() -> {
try (PrintStream ps = new PrintStream(pos)) {
for (int i = 0; i < 10; i++) {
ps.println("數據 " + i);
Thread.sleep(100);
}
}
}).start();
// 消費者線程
try (BufferedReader br = new BufferedReader(new InputStreamReader(pis))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println("收到: " + line);
}
}
| File API | NIO.2 替代 | 優勢 |
|---|---|---|
new File(path) | Paths.get(path) / Path.of(path) (JDK 11+) | 不可變、線程安全、支援 URI |
file.exists() | Files.exists(path) | 支援鏈接選項、異常豐富 |
file.mkdirs() | Files.createDirectories(path) | 原子性、拋具體異常 |
file.delete() | Files.deleteIfExists(path) | 不存在不報錯 |
file.listFiles() | Files.list(path) / Files.walk(path) | Stream API、惰性、並行 |
file.length() | Files.size(path) | 拋異常而非返回 0 |
file.renameTo() | Files.move(src, target, REPLACE_EXISTING) | 跨文件系統、原子移動 |
new FileInputStream(file) | Files.newInputStream(path) | 選項控制 (SPARSE、DSYNC 等) |
File.separator / Paths.get(),絕不硬編碼 / 或 \\。File 不代表真實文件:new File("a.txt").exists() == false 合法,操作前必檢查 exists()。new File("data.txt") 基於 user.dir (啟動目錄),非類路徑。服務器部署時 user.dir 可能非預期。生產環境務必用絕對路徑或配置注入。..、/、\、:、*、?、"、<、>、|、null 字符,防止路徑遍歷攻擊。Files.copy/FileChannel.transferTo/MappedByteBuffer,避免全量讀入內存。FileChannel.lock() / tryLock(),釋放需同一進程/通道。System.setOut 影響全局,多線程/測試環境易幹擾。測試用 System.setOut + try-with-resources 恢復,生產用日誌框架。Path/Files。FileInputStream/OutputStream、Socket 內部持有。三大標準流 in/out/err 對應 0/1/2。System.setOut/err、ProcessBuilder.redirect、PipedInputStream/OutputStream。理解 File 與 FileDescriptor 的分工邊界,掌握 NIO.2 現代 API,能讓文件操作代碼更安全、高效、可維護。