CodeGym /Các khóa học /JAVA 25 SELF /NIO2: Files, Paths, Files.walk: duyệt hệ thống tệp

NIO2: Files, Paths, Files.walk: duyệt hệ thống tệp

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. NIO2: tìm hiểu kỹ hơn

Chúng ta đã làm quen với NIO2, bây giờ cùng ôn tập và đào sâu kiến thức về thư viện hữu ích này để làm việc với tệp và thư mục.

Trước đây trong Java chỉ có lớp File. Nó có thể kiểm tra sự tồn tại của tệp, tạo và xóa tệp/thư mục, lấy danh sách tệp trong thư mục. Nhưng nó có nhiều hạn chế:

  • làm việc với đường dẫn bất tiện, đặc biệt khi cần tính đến các hệ điều hành khác nhau (C:\Users\user\file.txt trên Windows và /home/user/file.txt trên Linux);
  • không có hỗ trợ chuẩn cho symbolic link, quyền truy cập và thuộc tính tệp;
  • khả năng duyệt cây thư mục bị hạn chế;
  • xử lý lỗi chưa tốt.

Từ khi NIO2 (New Input/Output, phiên bản 2) xuất hiện trong Java 7, cuộc sống của lập trình viên trở nên dễ dàng và thú vị hơn. Hiện có sẵn:

  • lớp Path để làm việc thuận tiện với đường dẫn tới tệp và thư mục;
  • lớp Files, cung cấp các thao tác chính: đọc, ghi, sao chép, xóa, lấy thông tin về tệp;
  • interface FileVisitor và các phương thức như Files.walk cho phép duyệt hệ thống tệp một cách dễ dàng và linh hoạt.

Vì sao điều này quan trọng?

  • Đa nền tảng: Cùng một đoạn mã chạy trên Windows, Linux, macOS mà không cần nghĩ đến dấu phân tách (/ hay \).
  • An toàn và tiện lợi: Nhiều thông tin lỗi hơn, ít “ma thuật” và bất ngờ không mong muốn.
  • Mạnh mẽ: Có thể xử lý các thư mục rất lớn và thậm chí duyệt đệ quy với lọc và xử lý song song.

2. Các lớp chính: Path và Files

Lớp Path

Path — là biểu diễn hiện đại của đường dẫn đến tệp hoặc thư mục. Nó không nhất thiết trỏ đến tệp thực sự tồn tại — chỉ là đường dẫn để làm việc thuận tiện.

Tạo Path

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

Path path1 = Paths.get("file.txt"); // đường dẫn tương đối
Path path2 = Paths.get("/home/user/file.txt"); // đường dẫn tuyệt đối
Path path3 = Path.of("mydir", "subdir", "file.txt"); // từ Java 11+

Thực tế: Path không phụ thuộc hệ điều hành. Hãy quên việc tự ghép chuỗi bằng / hay \!

Chuyển thành chuỗi

System.out.println(path1.toString());

Lấy thư mục cha và tên tệp

Path parent = path1.getParent(); // có thể là null với các đường dẫn tương đối
Path fileName = path1.getFileName(); // chỉ tên tệp

Lớp Files

Files — tập hợp các phương thức static cho mọi thao tác với tệp và thư mục:

  • Kiểm tra tồn tại: Files.exists(path)
  • Đọc và ghi tệp: Files.readAllBytes(path), Files.write(path, bytes)
  • Lấy thông tin: Files.size(path), Files.getLastModifiedTime(path)
  • Sao chép, xóa, di chuyển: Files.copy, Files.delete, Files.move

Ví dụ:

import java.nio.file.Files;
import java.nio.file.Path;

Path path = Path.of("file.txt");
if (Files.exists(path)) {
    System.out.println("Tệp tồn tại!");
    System.out.println("Kích thước: " + Files.size(path) + " byte");
    System.out.println("Sửa đổi lần cuối: " + Files.getLastModifiedTime(path));
} else {
    System.out.println("Không tìm thấy tệp.");
}

3. Duyệt hệ thống tệp: Files.walk và các hàm liên quan

Vấn đề của cách cũ

Trong API cũ, để duyệt tất cả tệp trong thư mục và các thư mục con, phải viết hàm đệ quy, tự kiểm tra đâu là tệp, đâu là thư mục, và cẩn thận tránh đệ quy vô hạn. Điều đó không chỉ mệt mỏi mà còn rất dễ sai sót.

Cách hiện đại: Files.walk

Files.walk(Path start) trả về Stream<Path> — luồng tất cả tệp và thư mục bắt đầu từ đường dẫn chỉ định, bao gồm mọi thư mục con. Bây giờ duyệt hệ thống tệp chỉ là làm việc với Stream!

Ví dụ: In ra tất cả tệp và thư mục

import java.nio.file.*;

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.forEach(System.out::println);
}

Tất cả các đường dẫn sẽ được in ra: cả tệp và thư mục, bắt đầu từ mydir.

Ví dụ: Chỉ các tệp (không gồm thư mục)

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.filter(Files::isRegularFile)
         .forEach(System.out::println);
}

Phương thức Files.isRegularFile(path) trả về true chỉ cho tệp thường (không phải thư mục, không phải symlink).

Ví dụ: Tìm tệp theo phần mở rộng

Giả sử cần tìm tất cả các tệp .txt trong thư mục và các thư mục con:

try (var paths = Files.walk(Path.of("mydir"))) {
    paths.filter(Files::isRegularFile)
         .filter(path -> path.toString().endsWith(".txt"))
         .forEach(System.out::println);
}

Ví dụ: Tính tổng kích thước của tất cả tệp

long totalSize = 0;
try (var paths = Files.walk(Path.of("mydir"))) {
    totalSize = paths.filter(Files::isRegularFile)
                     .mapToLong(path -> {
                         try {
                             return Files.size(path);
                         } catch (Exception e) {
                             System.err.println("Lỗi khi đọc kích thước: " + path);
                             return 0L;
                         }
                     })
                     .sum();
}
System.out.println("Tổng kích thước tệp: " + totalSize + " byte");

Quan trọng!

  • Phương thức Files.walk trả về stream cần được đóng (nó triển khai AutoCloseable). Vì vậy hãy dùng try-with-resources!
  • Mặc định độ sâu duyệt — đến tận cùng (mọi thư mục con). Có thể giới hạn độ sâu: Files.walk(path, maxDepth)

4. Bài tập thực hành

Bài 1: Tìm tất cả hình ảnh trong thư mục

Cần tìm tất cả các tệp có phần mở rộng .jpg, .png, .gif trong thư mục images và in tên của chúng.

import java.nio.file.*;
import java.util.Set;

Set<String> extensions = Set.of(".jpg", ".png", ".gif");

try (var paths = Files.walk(Path.of("images"))) {
    paths.filter(Files::isRegularFile)
         .filter(path -> {
             String name = path.getFileName().toString().toLowerCase();
             return extensions.stream().anyMatch(name::endsWith);
         })
         .forEach(System.out::println);
}

Bài 2: Sao chép tất cả tệp .txt sang thư mục khác

import java.nio.file.*;

Path sourceDir = Path.of("src");
Path destDir = Path.of("dest");

try (var paths = Files.walk(sourceDir)) {
    paths.filter(Files::isRegularFile)
         .filter(path -> path.toString().endsWith(".txt"))
         .forEach(path -> {
             try {
                 Path relative = sourceDir.relativize(path);
                 Path target = destDir.resolve(relative);
                 Files.createDirectories(target.getParent());
                 Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
                 System.out.println("Đã sao chép: " + path + " -> " + target);
             } catch (Exception e) {
                 System.err.println("Lỗi sao chép: " + path);
             }
         });
}

Ở đây chúng ta giữ nguyên cấu trúc các thư mục con.

5. Những điểm hữu ích

Ưu điểm của NIO2

Đa nền tảng

Path tự xử lý dấu phân tách thư mục. Mã của bạn sẽ chạy như nhau trên Windows, Linux, macOS.

Xử lý theo luồng

Các phương thức như Files.walk trả về một luồng (Stream<Path>) có thể lọc, biến đổi, gom vào các collection — mọi thứ mà Stream API hỗ trợ.

Làm việc với thư mục lớn

API cũ có thể “gục” nếu có quá nhiều tệp (ví dụ, 100 000 ảnh). NIO2 xử lý các trường hợp này dễ dàng vì không tải tất cả vào bộ nhớ ngay lập tức.

Hỗ trợ symlink, thuộc tính, quyền truy cập

Có thể biết đường dẫn có phải là liên kết tượng trưng (Files.isSymbolicLink(path)), lấy quyền truy cập (Files.getPosixFilePermissions(path)), biết chủ sở hữu tệp và nhiều điều khác.

So sánh API cũ và mới

Thao tác API cũ (File) API mới (Path, Files)
Kiểm tra tồn tại
file.exists()
Files.exists(path)
Lấy kích thước
file.length()
Files.size(path)
Danh sách tệp trong thư mục
file.listFiles()
Files.list(path)
Duyệt đệ quy Đệ quy thủ công
Files.walk(path)
Sao chép tệp file.renameTo() (không đáng tin)
Files.copy(src, dest)
Lấy phần mở rộng Phân tích chuỗi thủ công
path.getFileName().toString()
Lấy thư mục cha
file.getParentFile()
path.getParent()
Làm việc với quyền Hầu như không có
Files.getPosixFilePermissions(path)

Đặc điểm quan trọng

Kiểm tra loại tệp

  • Files.isRegularFile(path) — tệp thường
  • Files.isDirectory(path) — thư mục
  • Files.isSymbolicLink(path) — symlink

Làm việc với thư mục lớn

  • Không nên gom tất cả đường dẫn vào một danh sách: hãy làm việc với luồng (Stream<Path>) và xử lý dần khi chúng đến.
  • Sau khi kết thúc, phải đóng luồng (try-with-resources).

Ngoại lệ

  • Hầu hết các phương thức có thể ném IOException — đừng quên xử lý lỗi (hoặc ném lên trên).

Giới hạn độ sâu duyệt

try (var paths = Files.walk(Path.of("mydir"), 2)) { // chỉ 2 cấp
    // ...
}

6. Lỗi thường gặp khi làm việc với NIO2

Lỗi №1: quên đóng stream walk. Nếu không dùng try-with-resources, có thể gây rò rỉ tài nguyên — luồng hệ thống tệp sẽ vẫn mở. Luôn dùng cấu trúc try (var paths = Files.walk(...)) { ... }.

Lỗi №2: không kiểm tra rằng đường dẫn là thư mục. Nếu truyền vào Files.walk một đường dẫn tới tệp chứ không phải thư mục, có thể nhận hành vi bất ngờ hoặc lỗi.

Lỗi №3: không xử lý ngoại lệ. Hầu như tất cả các phương thức của NIO2 đều có thể ném IOException. Đừng bỏ qua — ít nhất hãy in thông báo cho người dùng hoặc ghi log.

Lỗi №4: nhầm lẫn về dấu phân tách đường dẫn. Nếu bạn tự ghép đường dẫn bằng / hay \, thì không cần thiết! Hãy dùng Path.of(...) hoặc resolve(...) — chúng sẽ xử lý đúng trên mọi hệ điều hành.

Lỗi №5: cố đọc cả một thư mục khổng lồ “vào bộ nhớ”. Đừng gom tất cả đường dẫn vào một danh sách nếu có quá nhiều tệp — hãy làm việc với luồng (Stream<Path>) và xử lý dần khi chúng tới.

Lỗi №6: quên tính đa nền tảng. Đừng hardcode các đường dẫn tuyệt đối theo phong cách Windows hoặc Unix. Hãy dùng Path và các thao tác như resolve/relativize — chúng làm đúng trên mọi hệ điều hành.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION