CodeGym /Các khóa học /JAVA 25 SELF /Các thao tác hàng loạt trên tệp

Các thao tác hàng loạt trên tệp

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

1. Duyệt tệp trong thư mục

Lấy danh sách tệp: Files.list và Files.walk

Trong Java có hai phương thức rất hữu ích để làm việc với nội dung thư mục:

  • Files.list(Path) — trả về một stream (Stream<Path>) của tất cả tệp và thư mục cấp trên cùng trong thư mục đã cho.
  • Files.walk(Path) — trả về stream của tất cả tệp và thư mục trong thư mục đó và mọi thư mục con của nó (đệ quy).

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

import java.nio.file.*;
import java.io.IOException;

public class ListFilesExample {
    public static void main(String[] args) throws IOException {
        Path dir = Paths.get("data");

        // Lấy stream của tất cả tệp và thư mục cấp trên cùng
        try (var stream = Files.list(dir)) {
            stream.forEach(System.out::println);
        }
    }
}

Nếu bạn muốn đi qua tất cả các thư mục con (đệ quy), hãy dùng Files.walk:

try (var stream = Files.walk(dir)) {
    stream.forEach(System.out::println);
}

Lưu ý:
Files.walk có thể trả về rất nhiều tệp nếu cấu trúc lớn — đừng chạy nó ở gốc ổ đĩa nếu bạn không muốn thấy “danh sách của cả vũ trụ”!

Trực quan hóa: sự khác nhau giữa list và walk

Phương thức Trả về gì
Files.list
Chỉ nội dung của thư mục hiện tại
Files.walk
Mọi tệp và thư mục, bao gồm cả thư mục lồng nhau

2. Lọc tệp

Một trong những ưu điểm lớn của việc làm việc với stream (Stream<Path>) là khả năng lọc tệp theo bất kỳ tiêu chí nào: phần mở rộng, tên, kích thước, ngày tạo, v.v.

Lọc theo phần mở rộng

Giả sử chúng ta chỉ cần các tệp .txt. Có thể lọc như sau:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .forEach(System.out::println);
}

Lọc theo tên

Chúng ta muốn tất cả các tệp có tên bắt đầu bằng "report":

stream
    .filter(path -> path.getFileName().toString().startsWith("report"))
    .forEach(System.out::println);

Lọc theo kích thước

Chỉ lấy các tệp “nặng” (lớn hơn 1 MB):

stream
    .filter(path -> {
        try {
            return Files.size(path) > 1_000_000;
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

Lọc theo ngày

Ví dụ, các tệp được sửa đổi trong 7 ngày gần đây:

import java.time.*;
import java.nio.file.attribute.*;

stream
    .filter(path -> {
        try {
            FileTime lastModified = Files.getLastModifiedTime(path);
            return lastModified.toInstant().isAfter(Instant.now().minus(Duration.ofDays(7)));
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

Kết hợp: lọc tổng hợp

Có thể kết hợp các bộ lọc “như gọi món”: ví dụ, tất cả tệp .log cũ hơn một tháng và lớn hơn 10 KB:

stream
    .filter(path -> path.toString().endsWith(".log"))
    .filter(path -> {
        try {
            return Files.size(path) > 10_000;
        } catch (IOException e) {
            return false;
        }
    })
    .filter(path -> {
        try {
            FileTime lastModified = Files.getLastModifiedTime(path);
            return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(30)));
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

3. Sao chép và xóa tệp hàng loạt

Các thao tác hàng loạt rất đơn giản: lấy một stream tệp, sau đó áp dụng hàm cần thiết cho từng phần tử. Điều quan trọng là đừng quên các lỗi có thể xảy ra (ví dụ tệp đang bị khóa hoặc đã tồn tại).

Sao chép tất cả tệp từ một thư mục sang thư mục khác

import java.nio.file.StandardCopyOption;

Path sourceDir = Paths.get("data");
Path targetDir = Paths.get("backup");

try (var stream = Files.list(sourceDir)) {
    stream.forEach(path -> {
        Path targetPath = targetDir.resolve(path.getFileName());
        try {
            Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("Đã sao chép: " + path.getFileName());
        } catch (IOException e) {
            System.out.println("Lỗi sao chép " + path.getFileName() + ": " + e.getMessage());
        }
    });
}

Xóa tất cả tệp tạm (*.tmp)

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("Đã xóa: " + path.getFileName());
            } catch (IOException e) {
                System.out.println("Lỗi xóa " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Sử dụng Stream API để xử lý tập hợp đường dẫn

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .sorted((p1, p2) -> {
            try {
                return Long.compare(Files.size(p2), Files.size(p1)); // theo kích thước, giảm dần
            } catch (IOException e) {
                return 0;
            }
        })
        .limit(5) // chỉ 5 tệp lớn nhất
        .forEach(System.out::println);
}

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

Tìm và xóa các tệp cũ hơn một mốc thời gian nhất định

Xóa tất cả tệp không được sửa đổi hơn một năm:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> {
            try {
                FileTime lastModified = Files.getLastModifiedTime(path);
                return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(365)));
            } catch (IOException e) {
                return false;
            }
        })
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("Đã xóa: " + path.getFileName());
            } catch (IOException e) {
                System.out.println("Lỗi xóa " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Đổi tên tệp hàng loạt theo mẫu

Giả sử cần thêm tiền tố .txt vào mỗi tệp với prefix "old_":

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .forEach(path -> {
            Path newPath = path.resolveSibling("old_" + path.getFileName());
            try {
                Files.move(path, newPath);
                System.out.println("Đã đổi tên: " + path.getFileName() + " -> " + newPath.getFileName());
            } catch (IOException e) {
                System.out.println("Lỗi đổi tên " + path.getFileName() + ": " + e.getMessage());
            }
        });
}

Sao chép tất cả tệp từ mọi thư mục con (đệ quy)

Dùng Files.walk để duyệt mọi tệp trong tất cả thư mục con:

try (var stream = Files.walk(sourceDir)) {
    stream
        .filter(Files::isRegularFile)
        .forEach(path -> {
            Path relative = sourceDir.relativize(path);
            Path targetPath = targetDir.resolve(relative);
            try {
                Files.createDirectories(targetPath.getParent());
                Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
                System.out.println("Đã sao chép: " + path + " -> " + targetPath);
            } catch (IOException e) {
                System.out.println("Lỗi sao chép " + path + ": " + e.getMessage());
            }
        });
}

5. Những điểm quan trọng và lưu ý

Xử lý lỗi trong các thao tác hàng loạt

Trong các thao tác hàng loạt hầu như luôn có các tệp “có vấn đề”: không có quyền truy cập, tệp đang bị khóa, đường dẫn quá dài, tệp đã tồn tại, v.v. Nếu không xử lý ngoại lệ bên trong vòng lặp, chương trình có thể “ngã” ngay ở lỗi đầu tiên. Vì vậy hãy luôn sử dụng try-catch bên trong forEach, để “nuốt” lỗi của từng tệp nhưng vẫn xử lý các tệp còn lại.

Ảnh hưởng của đệ quy đến hiệu năng và ngăn xếp lời gọi

Khi sử dụng Files.walk cho các thư mục lớn bạn có thể nhận một lượt duyệt rất dài nếu tự viết đệ quy thủ công. May mắn thay, chính phương thức này được triển khai hiệu quả và không gây tràn stack. Nhưng nếu bạn viết hàm đệ quy của riêng mình để xóa/sao chép thư mục, hãy cẩn thận: với cấu trúc lồng nhau khổng lồ, stack có thể “vỡ”.

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

  • Hãy dùng Files.walk cẩn trọng: nếu thư mục có hàng nghìn tệp, thao tác có thể tốn thời gian và “ngốn” nhiều bộ nhớ.
  • Nếu chỉ cần xử lý cấp trên cùng — hãy dùng Files.list.
  • Để tìm tệp theo mẫu, hãy lọc theo tên (endsWith, matches, v.v.).

Ví dụ: “Dọn sạch thư mục khỏi tệp tạm”

Path tempDir = Paths.get("data/tmp");

try (var stream = Files.walk(tempDir)) {
    stream
        .filter(Files::isRegularFile)
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("Đã xóa: " + path);
            } catch (IOException e) {
                System.out.println("Lỗi xóa " + path + ": " + e.getMessage());
            }
        });
}

Các phương thức chính cho thao tác hàng loạt

Thao tác Phương thức/cách tiếp cận Đặc điểm
Lấy tất cả tệp
Files.list, Files.walk
walk — đệ quy, list — chỉ cấp trên cùng
Lọc theo phần mở rộng
filter(path -> ...)
Có thể kết hợp với các bộ lọc khác
Sao chép một nhóm tệp
forEach + Files.copy
Kiểm tra sự tồn tại của thư mục đích
Xóa một nhóm tệp
forEach + Files.delete
Nên dùng try-catch cho từng thao tác
Đổi tên hàng loạt
forEach + Files.move
Đổi tên thực chất là move sang tên mới

6. Lỗi thường gặp khi thao tác hàng loạt

Lỗi số 1: Ngoại lệ không được xử lý bên trong forEach.
Một vấn đề rất phổ biến: nếu không bọc mỗi thao tác trong try-catch, chương trình sẽ “ngã” ngay ở tệp gặp vấn đề đầu tiên và các tệp còn lại sẽ không được xử lý.

Lỗi số 2: Cố xóa/sao chép thư mục như tệp (hoặc ngược lại).
Các phương thức Files.deleteFiles.copy xử lý tệp và thư mục theo cách khác nhau. Đừng nhầm lẫn! Ví dụ, cố xóa thư mục không rỗng bằng phương thức tiêu chuẩn sẽ gây lỗi.

Lỗi số 3: Tạo đường dẫn đích sai khi sao chép.
Nếu tạo đường dẫn đích mà không xét tới cấu trúc (ví dụ không sử dụng relativize), bạn có thể ghi đè tệp hoặc nhận cấu trúc không như mong muốn trong bản sao.

Lỗi số 4: Mở quá nhiều tệp cùng lúc.
Nếu bạn mở stream để đọc/ghi bên trong vòng lặp, đừng quên đóng chúng! Tốt hơn là sử dụng try-with-resources.

Lỗi số 5: Duyệt đệ quy không giới hạn độ sâu.
Khi sử dụng Files.walk trên các thư mục rất lớn và sâu, có thể gặp vấn đề về hiệu năng và bộ nhớ. Nếu cần giới hạn độ sâu — hãy dùng phiên bản có tham số độ sâu: Files.walk(dir, depth).

Lỗi số 6: Vô tình chạm vào tệp ẩn/hệ thống.
Nếu ứng dụng làm việc với tệp của người dùng, bạn có thể vô tình chạm tới tệp ẩn hoặc hệ thống. Để lọc chúng, hãy sử dụng phương thức Files.isHidden(path).

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