首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 文件與目錄操作完整指南:NIO.2、Stream、常用工具類與避坑實錄

     編輯:Java 相關知識

前言

文件操作是 Java 開發的基礎功,但 java.io.File (Legacy IO) 與 java.nio.file (NIO.2) 並存導致 API 混亂、異常處理繁瑣、編碼問題頻發。本文系統梳理 **NIO.2 為主、Legacy 為輔** 的現代最佳實踐,涵蓋建立/刪除/複製/移動/遍歷/讀寫/屬性/監聽全生命週期,並給出生產級工具類與避坑清單。

一、API 選型:NIO.2 完全取代 Legacy IO

場景推薦 API (NIO.2)Legacy IO (棄用/遷移中)
路徑表示Path, Paths.get()File
建立/刪除/複製/移動Files.createFile/Dir/delete/copy/moveFile.createNewFile/mkdir/delete/renameTo
遍歷目錄樹Files.walk/walkFileTreeFile.listFiles() 遞歸
讀取文本 (小文件)Files.readAllLines/readString (Java 11+)BufferedReader + FileReader
讀取文本 (大文件/流式)Files.lines() → StreamBufferedReader
寫入文本Files.write/writeStringFileWriter/BufferedWriter
二進制讀寫Files.readAllBytes/newInputStream/newOutputStreamFileInputStream/OutputStream
屬性讀取Files.readAttributes/BasicFileAttributesFile.length/lastModified/isDirectory
文件監聽WatchService無

二、核心操作代碼模板 (Java 11+)

2.1 路徑構建與正規化


Path path = Paths.get("/data", "logs", "app.log").normalize().toAbsolutePath();
// 正規化:解析 . .. 符號鏈接 → 絕對路徑
// 跨平台分隔符自動處理,Windows 也可用正斜線

2.2 建立目錄與文件 (原子性、屬性設置)


// 建立多級目錄,存在不報錯
Files.createDirectories(Paths.get("/data/logs/2024/01"));

// 建立文件,可設置權限 (POSIX)
Files.createFile(path, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-r--r--")));

// 檢查存在性
if (Files.notExists(path)) { ... }
// 或原子建立:CREATE_NEW 失敗拋 FileAlreadyExistsException
Files.createFile(path, StandardOpenOption.CREATE_NEW);

2.3 刪除:文件/空目錄/非空目錄樹


// 刪除文件/空目錄,不存在返回 false 不拋異常
Files.deleteIfExists(path);

// 遞歸刪除非空目錄 (Java 8+)
Files.walkFileTree(dir, new SimpleFileVisitor() {
    @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        Files.delete(file); return FileVisitResult.CONTINUE;
    }
    @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        Files.delete(dir); return FileVisitResult.CONTINUE;
    }
});

// Java 11+ 一行刪除目錄樹 (內部實現同上)
Files.deleteIfExists(dir); // 僅空目錄
// 非空目錄仍需 walkFileTree 或引入 Apache Commons IO: FileUtils.deleteDirectory(dir.toFile())

2.4 複製與移動 (原子、覆蓋、屬性複製)


Path src = Paths.get("/data/source.txt");
Path dst = Paths.get("/data/backup/target.txt");

// 複製:覆蓋、複製屬性、原子移動
Files.copy(src, dst,
    StandardCopyOption.REPLACE_EXISTING,
    StandardCopyOption.COPY_ATTRIBUTES,
    LinkOption.NOFOLLOW_LINKS);

// 移動:同檔案系統原子重命名,跨檔案系統複製+刪除
Files.move(src, dst,
    StandardCopyOption.REPLACE_EXISTING,
    StandardCopyOption.ATOMIC_MOVE); // 同卷原子,跨卷忽略

2.5 目錄遍歷三種模式


// 1. 深度優先流式遍歷 (內存友好,可中斷)
try (Stream stream = Files.walk(root, 3, FileVisitOption.FOLLOW_LINKS)) {
    stream.filter(Files::isRegularFile)
          .filter(p -> p.toString().endsWith(".log"))
          .forEach(System.out::println);
}

// 2. 訪問者模式 (完全控制、可修改樹)
Files.walkFileTree(root, new SimpleFileVisitor() {
    @Override public FileVisitResult visitFileFailed(Path file, IOException exc) {
        log.warn("無法訪問:{}", file, exc); return FileVisitResult.CONTINUE; // 跳過錯誤繼續
    }
    @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        if (dir.getFileName().toString().startsWith(".")) return FileVisitResult.SKIP_SUBTREE; // 跳過隱藏目錄
        return FileVisitResult.CONTINUE;
    }
});

// 3. 只列一層 (ls 效果)
try (DirectoryStream stream = Files.newDirectoryStream(dir, "*.log")) {
    for (Path entry : stream) { ... }
}

2.6 文本讀寫:編碼、大小文件、Stream 處理


// 小文件一次性讀取 (Java 11+)
List lines = Files.readAllLines(path, StandardCharsets.UTF_8);
String content = Files.readString(path, StandardCharsets.UTF_8); // Java 11+

// 大文件流式處理 (自動關閉 Stream)
try (Stream lines = Files.lines(path, StandardCharsets.UTF_8)) {
    long errorCount = lines.filter(l -> l.contains("ERROR")).count();
}

// 寫入:原子寫入 (寫臨時文件 + 原子 move)
Path tmp = Files.createTempFile(path.getParent(), "tmp", ".log");
Files.write(tmp, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW);
Files.move(tmp, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);

// 追加寫入
Files.write(path, List.of("new line"), StandardCharsets.UTF_8,
    StandardOpenOption.CREATE, StandardOpenOption.APPEND);

2.7 二進制讀寫:InputStream/OutputStream + try-with-resources


// 讀取全部位元組 (小文件)
byte[] data = Files.readAllBytes(path);

// 流式複製 (大文件,緩衝區 8KB)
try (InputStream in = Files.newInputStream(src);
     OutputStream out = Files.newOutputStream(dst,
         StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
    byte[] buffer = new byte[8192];
    int len;
    while ((len = in.read(buffer)) != -1) out.write(buffer, 0, len);
}

// 內存映射 (極大文件、隨機讀寫、高效能)
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
    MappedByteBuffer mbb = ch.map(FileChannel.MapMode.READ_ONLY, 0, ch.size());
    // 直接操作 mbb,OS 負責頁面調度
}

2.8 文件屬性與權限


BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
attrs.size()           // 大小
attrs.creationTime()   // 建立時間
attrs.lastModifiedTime() // 修改時間
attrs.isDirectory()    // 是否目錄
attrs.isRegularFile()  // 是否普通文件
attrs.isSymbolicLink() // 是否符號鏈接

// POSIX 權限 (Linux/macOS)
Set perms = Files.getPosixFilePermissions(path);
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rwxr-x---"));

// Windows ACL (Java 17+)
AclFileAttributeView aclView = Files.getFileAttributeView(path, AclFileAttributeView.class);
List acl = aclView.getAcl();

2.9 文件監聽


WatchService watcher = FileSystems.getDefault().newWatchService();
Path dir = Paths.get("/data/watch");
dir.register(watcher,
    StandardWatchEventKinds.ENTRY_CREATE,
    StandardWatchEventKinds.ENTRY_DELETE,
    StandardWatchEventKinds.ENTRY_MODIFY);

while (true) {
    WatchKey key = watcher.take(); // 阻塞
    for (WatchEvent event : key.pollEvents()) {
        Path changed = dir.resolve((Path) event.context());
        System.out.println(event.kind() + " -> " + changed);
    }
    boolean valid = key.reset();
    if (!valid) break; // 目錄不可訪問
}

三、生產級工具類


public final class FileUtils {
    private FileUtils() {}

    /** 原子寫入文本:臨時文件 + 原子移動,防止寫入中斷產生髒文件 */
    public static void writeStringAtomically(Path path, String content, Charset charset) throws IOException {
        Path tmp = Files.createTempFile(path.getParent(), "atomic-", ".tmp");
        try {
            Files.writeString(tmp, content, charset, StandardOpenOption.CREATE_NEW);
            Files.move(tmp, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
        } catch (IOException e) {
            Files.deleteIfExists(tmp); // 清理臨時文件
            throw e;
        }
    }

    /** 複製目錄樹:保留屬性、符號鏈接選項、進度回調 */
    public static long copyDirectory(Path src, Path dst, CopyOption... options) throws IOException {
        AtomicLong copied = new AtomicLong();
        Files.walkFileTree(src, new SimpleFileVisitor() {
            @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                Path target = dst.resolve(src.relativize(dir));
                Files.createDirectories(target, Arrays.stream(options).filter(o -> o instanceof LinkOption).toArray(CopyOption[]::new));
                return FileVisitResult.CONTINUE;
            }
            @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Path target = dst.resolve(src.relativize(file));
                Files.copy(file, target, options);
                copied.addAndGet(attrs.size());
                return FileVisitResult.CONTINUE;
            }
        });
        return copied.get();
    }

    /** 計算目錄大小 (位元組) */
    public static long sizeOf(Path path) throws IOException {
        if (Files.isRegularFile(path)) return Files.size(path);
        return Files.walk(path).filter(Files::isRegularFile).mapToLong(p -> {
            try { return Files.size(p); } catch (IOException e) { return 0L; }
        }).sum();
    }
}

四、避坑實錄:血淚教訓總結

  1. 編碼不指定 → 平台預設編碼 (Windows GBK, Linux UTF-8) → 跨平台亂碼:所有文本讀寫強制指定 StandardCharsets.UTF_8。
  2. File.delete() 失敗不拋異常 → 靜默失敗:改用 Files.deleteIfExists() 判斷返回值,或 Files.delete() 捕獲 NoSuchFileException/DirectoryNotEmptyException。
  3. renameTo() 跨檔案系統失敗、非原子:改用 Files.move(..., ATOMIC_MOVE)。
  4. 大文件 readAllBytes/readAllLines → OOM:超過 100MB 必用流式 Files.lines() / InputStream / MappedByteBuffer。
  5. 遍歷目錄遇到權限不足/符號鏈接循環 → 拋異常中斷:walkFileTree 覆寫 visitFileFailed 返回 CONTINUE,preVisitDirectory 檢測循環返回 SKIP_SUBTREE。
  6. Windows 文件鎖定導致刪除/移動失敗:Files.deleteIfExists 返回 false,需重試或排查佔用進程 (Handle/Process Explorer)。
  7. 相對路徑在工作目錄變更後失效:統一用 path.toAbsolutePath().normalize() 落盤/傳遞。

五、總結

  • NIO.2 (Path, Files, FileChannel) 為主力,Legacy IO (File, FileInputStream) 僅維護舊代碼。
  • 編碼、原子性、資源關閉、異常處理**四件套**寫進肌肉記憶。
  • 工具類封裝原子寫入、目錄複製、大小計算,團隊統一依賴,杜絕重複造輪子。
  • 避坑清單印在工位,Code Review 逐項核對——文件操作零事故,是系統穩定性的基石。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved