Duyệt Cây Thư Mục
(Walking the File Tree)
Bạn có bao giờ cần viết một chương trình để kiểm tra hoặc xử lý lần lượt tất cả các tệp trong một thư mục, bao gồm cả các thư mục con bên trong nó không? Ví dụ: bạn muốn xóa hết các tệp có đuôi .class trong toàn bộ cây thư mục, hoặc tìm những tệp nào chưa được mở trong suốt một năm qua. Để làm được việc này, bạn có thể dùng interface FileVisitor trong Java.
Interface FileVisitor<T>
FileVisitor là một interface trong Java dùng để duyệt qua cây thư mục (file tree). Interface này được sử dụng với phương thức Files.walkFileTree() để định nghĩa hành vi quyết định điều gì sẽ xảy ra ở các thời điểm quan trọng trong quá trình duyệt: khi một tệp được truy cập, trước khi một thư mục được truy cập, sau khi một thư mục được truy cập, hoặc khi xảy ra lỗi. Giao diện này có bốn phương thức tương ứng với các tình huống này:
↳ FileVisitResult preVisitDirectorypreVisitDirectory(T dir, BasicFileAttributes attrs): Được gọi trước khi các mục trong một thư mục được truy cập.
↳ FileVisitResult postVisitDirectorypostVisitDirectory(T dir, IOException exc): Được gọi sau khi tất cả các mục trong một thư mục đã được truy cập. Nếu gặp bất kỳ lỗi nào, ngoại lệ cụ thể sẽ được truyền cho phương thức.
↳ FileVisitResult visitFilevisitFile(T file, BasicFileAttributes attrs): Được gọi trên tệp đang được truy cập. BasicFileAttributes của tệp được truyền cho phương thức, hoặc bạn có thể sử dụng gói file attributes để đọc một tập hợp thuộc tính cụ thể. Ví dụ: bạn có thể chọn đọc DosFileAttributeView của tệp để xác định xem tệp có bit "ẩn" được đặt hay không.
↳ FileVisitResult visitFileFailedvisitFileFailed(T file, IOException exc): Được gọi khi tệp không thể truy cập được. Ngoại lệ cụ thể được truyền cho phương thức. Bạn có thể chọn ném ngoại lệ, in nó ra bảng điều khiển hoặc tệp nhật ký, v.v.
Nếu bạn không cần triển khai tất cả bốn phương thức FileVisitor, thay vì triển khai giao diện FileVisitor, bạn có thể mở rộng lớp SimpleFileVisitor. Lớp này, triển khai giao diện FileVisitor, truy cập tất cả các tệp trong một cây và ném IOError khi gặp lỗi. Bạn có thể mở rộng lớp này và chỉ ghi đè (override) các phương thức mà bạn yêu cầu.
Dưới đây là một ví dụ mở rộng SimpleFileVisitor để in tất cả các mục trong một cây tệp. Nó in mục đó cho dù mục đó là một tệp thông thường, một liên kết tượng trưng, một thư mục hoặc một loại tệp "không xác định" khác. Nó cũng in kích thước, tính bằng byte, của mỗi tệp. Bất kỳ ngoại lệ nào gặp phải đều được in ra bảng điều khiển.
Ví dụ: PrintFiles.java
import static java.nio.file.FileVisitResult.*;
public static class PrintFiles
extends SimpleFileVisitor<Path> {
// In thông tin về từng loại tệp.
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attr) {
if (attr.isSymbolicLink()) {
System.out.format("Liên kết tượng trưng: %s ", file);
} else if (attr.isRegularFile()) {
System.out.format("Tệp thông thường: %s ", file);
} else {
System.out.format("Khác: %s ", file);
}
System.out.println("(" + attr.size() + "bytes)");
return CONTINUE;
}
// In từng thư mục đã truy cập.
@Override
public FileVisitResult postVisitDirectory(Path dir,
IOException exc) {
System.out.format("Thư mục: %s%n", dir);
return CONTINUE;
}
// Nếu có lỗi khi truy cập tệp, thông báo cho người dùng.
// Nếu bạn không ghi đè phương thức này và xảy ra lỗi,
// một IOException sẽ được ném.
@Override
public FileVisitResult visitFileFailed(Path file,
IOException exc) {
System.err.println(exc);
return CONTINUE;
}
}
Khai báo interface FileVisitor trong Java
Để sử dụng interface FileVisitor, bạn cần import gói java.io bạn cần thêm câu lệnh import vào đầu file Java của mình. Gói này cung cấp các lớp và giao diện để thực hiện các hoạt động nhập xuất (I/O) trong Java.
Cú pháp câu lệnh import:
Cú pháp
java.nio.file.FileVisitor;
Cú pháp khai báo interface FileVisitor:
Cú pháp
public interface FileVisitor<T>
Dưới đây là giải thích chi tiết về cú pháp khai báo này:
↳ public: Đây là từ khóa chỉ định phạm vi truy cập, nghĩa là giao diện này có thể được truy cập từ bất kỳ đâu.
↳ interface: Đây là từ khóa cho biết rằng chúng ta đang khai báo một giao diện. Giao diện định nghĩa một tập hợp các phương thức mà một lớp (class) có thể triển khai (implement).
↳ FileVisitor: Đây là tên của giao diện.
↳ <T>: Đây là một tham số kiểu (type parameter), cho biết giao diện FileVisitor là một giao diện generic (tổng quát). Điều này có nghĩa là giao diện có thể được sử dụng với các kiểu dữ liệu khác nhau. Trong trường hợp của FileVisitor, T thường đại diện cho kiểu của đường dẫn tệp (ví dụ: Path).
Bắt đầu quá trình duyệt cây thư mục
Sau khi bạn đã triển khai FileVisitor của mình, Sau khi bạn đã tạo xong một FileVisitor, làm thế nào để bắt đầu việc "đi qua cây tệp"? Trong lớp Files của Java, có hai phương thức walkFileTree để làm việc này:
(1) Phương thức walkFileTree(Path, FileVisitor)
Phương thức này chỉ yêu cầu một điểm bắt đầu (Path - đường dẫn thư mục gốc) và một đối tượng FileVisitor bạn đã tạo. của bạn. Bạn có thể gọi FileVisitor từ lớp PrintFiles.java như sau:
Ví dụ: PrintFiles.java
Path startingDir = Paths.get("C:/ThuMuc"); // Thư mục gốc
PrintFiles pf = new PrintFiles();
Files.walkFileTree(startingDir, pf);
Chương trình sẽ tự động đi qua toàn bộ cây tệp từ startingDir, gọi các phương thức trong pf khi cần.
Dưới đây là chương trình dùng phương thức walkFileTree() đầu tiên để duyệt toàn bộ cây thư mục và in tên tệp trong java:
Ví dụ
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class PrintFiles extends SimpleFileVisitor<Path> {
// Xử lý khi gặp tệp
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("Tệp: " + file);
return FileVisitResult.CONTINUE;
}
// Xử lý khi gặp thư mục (trước khi duyệt)
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Thư mục: " + dir);
return FileVisitResult.CONTINUE;
}
// Xử lý khi gặp lỗi
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Không thể truy cập: " + file + " - " + exc);
return FileVisitResult.CONTINUE;
}
public static void main(String[] args) {
Path startingDir = Paths.get("C:/ThuMuc"); // Đường dẫn thư mục gốc
PrintFiles pf = new PrintFiles();
try {
Files.walkFileTree(startingDir, pf); // Duyệt cây thư mục
} catch (IOException e) {
System.err.println("Lỗi khi duyệt thư mục: " + e);
}
}
}
Kết quả của chương trình là
Tệp: C:\ThuMuc\file1.txt
Tệp: C:\ThuMuc\file2.docx
Thư mục: C:\ThuMuc\ThuMucCon1
Tệp: C:\ThuMuc\ThuMucCon1\file3.txt
.........
Lưu ý:
↳ Sử dụng lớp SimpleFileVisitor để đơn giản hóa việc xử lý các sự kiện khi duyệt.
↳ Phương thức visitFile() và preVisitDirectory() được gọi khi gặp tệp và thư mục.
↳ Phương thức visitFileFailed() xử lý các lỗi khi truy cập tệp.
(2) Phương thức walkFileTree(Path, Set<FileVisitOption>, int, FileVisitor)
Phương thức walkFileTree thứ hai này cho phép bạn chỉ định thêm giới hạn về số cấp đã truy cập và một tập hợp các enum FileVisitOption.
↳ Nếu bạn muốn đảm bảo rằng phương thức này duyệt toàn bộ cây tệp, bạn có thể chỉ định Integer.MAX_VALUE cho đối số độ sâu tối đa.
↳ Bạn có thể chỉ định enum FileVisitOption, FOLLOW_LINKS, để chỉ ra rằng các liên kết tượng trưng nên được theo dõi.
Đoạn mã này cho thấy cách phương thức bốn đối số có thể được gọi:
Ví dụ
Path startingDir = Paths.get("C:/ThuMuc");
EnumSet<FileVisitOption> opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS); // Theo dõi liên kết tượng trưng
Finder finder = new Finder("*.txt"); // Tìm tệp .txt
Files.walkFileTree(startingDir, opts, Integer.MAX_VALUE, finder);
Dưới đây là chương trình dùng phương thức walkFileTree() thứ hai tìm các tệp .txt và theo dõi liên kết tượng trưng trong java:
Ví dụ: Example.java
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
public class Finder extends SimpleFileVisitor<Path> {
private final String pattern;
public Finder(String pattern) {
this.pattern = pattern;
}
// Xử lý khi gặp tệp và kiểm tra điều kiện
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.getFileName().toString().endsWith(".txt")) {
System.out.println("Tìm thấy tệp: " + file);
}
return FileVisitResult.CONTINUE;
}
// Xử lý lỗi khi duyệt
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Không thể truy cập: " + file + " - " + exc);
return FileVisitResult.CONTINUE;
}
public static void main(String[] args) {
Path startingDir = Paths.get("C:/ThuMuc"); // Đường dẫn thư mục gốc
EnumSet<FileVisitOption> opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS); // Theo dõi liên kết tượng trưng
Finder finder = new Finder("*.txt"); // Tìm các tệp .txt
try {
Files.walkFileTree(startingDir, opts, Integer.MAX_VALUE, finder); // Duyệt cây thư mục
} catch (IOException e) {
System.err.println("Lỗi khi duyệt thư mục: " + e);
}
}
}
Kết quả của chương trình là
Tìm thấy tệp: C:\ThuMuc\ThuMucCon1\file3.txt
Tìm thấy tệp: C:\ThuMuc\ThuMucCon2\file5.txt
Lưu ý:
↳ Sử dụng EnumSet.of(FileVisitOption.FOLLOW_LINKS) để theo dõi liên kết tượng trưng.
↳ Integer.MAX_VALUE cho phép duyệt toàn bộ cây thư mục mà không giới hạn độ sâu.
↳ Trong visitFile, chỉ in ra các tệp có phần mở rộng .txt.
Những điểm quan trọng cần cân nhắc khi tạo FileVisitor
Khi thiết kế FileVisitor, bạn cần cân nhắc những điểm sau:
(1) Thứ tự duyệt cây thư mục
Java duyệt cây thư mục theo chiều sâu (depth-first). Không thể dự đoán thứ tự mà các thư mục con được duyệt.
(2) Xử lý khi thay đổi hệ thống tập tin
Nếu chương trình thay đổi hệ thống tập tin/thư mục (như xóa, sao chép), hãy cẩn thận trong cách triển khai FileVisitor:
Nếu bạn đang viết một lệnh xóa đệ quy, bạn xóa các tệp trong một thư mục trước khi xóa chính thư mục đó. Trong trường hợp này, bạn xóa thư mục trong postVisitDirectory.
Ví dụ
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return CONTINUE;
}
Nếu bạn đang viết một lệnh sao chép đệ quy, bạn tạo thư mục mới trong preVisitDirectory trước khi cố gắng sao chép các tệp vào đó (trong visitFiles). Nếu bạn muốn giữ lại các thuộc tính của thư mục nguồn (tương tự như lệnh cp -p của UNIX), bạn cần thực hiện điều đó sau khi các tệp đã được sao chép, trong postVisitDirectory. Ví dụ Copy cho thấy cách thực hiện điều này.
Ví dụ
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path targetDir = target.resolve(source.relativize(dir));
Files.createDirectories(targetDir);
return CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.copy(file, target.resolve(source.relativize(file)));
return CONTINUE;
}
Nếu bạn đang viết một lệnh tìm kiếm tệp, bạn thực hiện so sánh trong phương thức visitFile. Phương thức này tìm tất cả các tệp phù hợp với tiêu chí của bạn, nhưng nó không tìm thấy các thư mục. Nếu bạn muốn tìm cả tệp và thư mục, bạn cũng phải thực hiện so sánh trong phương thức preVisitDirectory hoặc postVisitDirectory. Ví dụ Find cho thấy cách thực hiện điều này.
Ví dụ
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".txt")) {
System.out.println("Found: " + file);
}
return CONTINUE;
}
(3) Theo dõi liên kết biểu tượng (Symbolic Links)
Mặc định, phương thức walkFileTree() không theo dõi symbolic links.
Nếu cần theo dõi, sử dụng FileVisitOption.FOLLOW_LINKS.
Lưu ý: Khi xóa, nên tránh theo dõi liên kết để tránh xóa nhầm tập tin không mong muốn.
(4) Xử lý vòng lặp liên kết (Circular Link)
Nếu cây thư mục có vòng lặp (do symbolic links), cần bắt ngoại lệ FileSystemLoopException trong visitFileFailed để tránh lỗi lặp vô hạn. Ví dụ xử lý lỗi:
Ví dụ
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
if (exc instanceof FileSystemLoopException) {
System.err.println("Vòng lặp liên kết được phát hiện: " + file);
} else {
System.err.format("Không thể xử lý: %s: %s%n", file, exc);
}
return CONTINUE;
}
(5) Kiểm soát luồng (Controlling the Flow)
Có thể bạn muốn duyệt cây tệp để tìm một thư mục cụ thể và khi tìm thấy, bạn muốn quá trình kết thúc. Hoặc có thể bạn muốn bỏ qua các thư mục cụ thể.
Các phương thức FileVisitor() trả về giá trị FileVisitResult. Bạn có thể hủy bỏ quá trình duyệt tệp hoặc kiểm soát xem một thư mục có được truy cập hay không bằng các giá trị bạn trả về trong các phương thức FileVisitor:
↳ CONTINUE: Cho biết rằng quá trình duyệt tệp nên tiếp tục. Nếu phương thức preVisitDirectory trả về CONTINUE, thư mục sẽ được truy cập.
↳ TERMINATE: Ngay lập tức hủy bỏ quá trình duyệt tệp. Không có phương thức duyệt tệp nào khác được gọi sau khi giá trị này được trả về.
↳ SKIP_SUBTREE: Khi preVisitDirectory trả về giá trị này, thư mục được chỉ định và các thư mục con của nó sẽ bị bỏ qua (như cắt bỏ cả nhánh cây).
↳ SKIP_SIBLINGS: Nếu phương thức preVisitDirectory() trả về giá trị này thì thư mục hiện tại và các thư mục con cùng cấp khác cũng bị bỏ qua. Nếu phương thức postVisitDirectory() trả về giá trị này, các thư mục cùng cấp còn lại cũng sẽ bị bỏ qua. Về cơ bản, không có gì xảy ra thêm trong thư mục được chỉ định.
Ví dụ 1: Dưới đây là một chương trình Java đơn giản, dễ hiểu, sử dụng phương thức Files.walkFileTree() để bỏ qua thư mục có tên "SCCS" khi duyệt qua các thư mục và tệp:
Ví dụ: Example.java
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import static java.nio.file.FileVisitResult.*;
public class Example {
public static void main(String[] args) {
// Thư mục bắt đầu duyệt
Path startingDir = Paths.get("C:/ThuMuc");
try {
Files.walkFileTree(startingDir, new SimpleFileVisitor<Path>() {
// Xử lý trước khi vào một thư mục
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
// Nếu thư mục tên là "SCCS", bỏ qua nó và các thư mục con
if (dir.getFileName().toString().equals("SCCS")) {
System.out.println("Bỏ qua thư mục: " + dir);
return SKIP_SUBTREE;
}
return CONTINUE; // Tiếp tục duyệt nếu không phải "SCCS"
}
// Xử lý khi gặp một tệp
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("Đã tìm thấy tệp: " + file);
return CONTINUE;
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
Kết quả của chương trình là
Nếu không, tiếp tục duyệt với CONTINUE.
Ví dụ 2: Dưới đây là một chương trình Java dừng ngay khi tìm thấy một tệp cụ thể, giả sử bạn muốn tìm tệp có tên "file.txt" trong thư mục "C:/ThuMuc":
Ví dụ: Example.java
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import static java.nio.file.FileVisitResult.*;
public class Example {
public static void main(String[] args) {
// Thư mục bắt đầu duyệt
Path startingDir = Paths.get("C:/ThuMuc");
// Tệp cần tìm
Path lookingFor = Paths.get("file.txt");
try {
Files.walkFileTree(startingDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
// So sánh tên tệp để kiểm tra
if (file.getFileName().equals(lookingFor.getFileName())) {
System.out.println("Đã tìm thấy tệp: " + file);
return TERMINATE; // Dừng duyệt khi đã tìm thấy
}
return CONTINUE; // Nếu chưa tìm thấy, tiếp tục duyệt
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
Kết quả của chương trình là
Nếu không, tiếp tục duyệt với CONTINUE.
Hy vọng rằng qua các ví dụ trên, bạn sẽ hiểu rõ hơn về cách duyệt cây thư mục trong Java.