CodeGym /Các khóa học /JAVA 25 SELF /Đọc tệp văn bản: theo dòng, toàn bộ

Đọc tệp văn bản: theo dòng, toàn bộ

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

1. Đọc tệp theo dòng: BufferedReader và “đồng sự”

Trước đây chúng ta đã làm quen với việc đọc theo từng byte: cách đó tiện cho định dạng nhị phân, nhưng với văn bản thì không phù hợp. Tệp gồm các ký tự phụ thuộc vào mã hóa. Vì vậy Java cung cấp những “lớp bọc” tiện lợi — FileReader, BufferedReader và các lớp khác, giúp biến luồng byte thành luồng ký tự và chuỗi dòng.

Hãy hình dung một tệp văn bản — có thể là log của chương trình, danh sách người dùng hoặc thậm chí cuốn tiểu thuyết đồ sộ “Chiến tranh và hòa bình”. Đôi khi cần đọc nhanh toàn bộ tệp, đôi khi — duyệt theo từng dòng, và đôi khi — lấy ra một dòng cụ thể.

Trong Java có vài cách để làm điều đó, và lựa chọn phụ thuộc vào kích thước tệp và yêu cầu. Nếu cần xử lý tệp theo từng dòng (ví dụ, đếm số dòng hoặc tìm một bản ghi), hãy dùng cách đọc theo dòng. Còn nếu tệp nhỏ — bạn có thể nạp toàn bộ vào bộ nhớ và làm việc với nó như một danh sách các dòng.

Tại sao đọc theo dòng lại tốt?

Với các tệp lớn, đọc theo dòng giúp tránh vấn đề về bộ nhớ. Nạp vào bộ nhớ một log cỡ gigabyte là ý tưởng tệ: rất dễ gặp OutOfMemoryError. Còn đọc tệp từng dòng lại rất tiết kiệm, ngay cả khi nó nặng hàng trăm megabyte.

Làm điều đó trong Java như thế nào?

Cách kinh điển — dùng BufferedReader (hoặc họ hàng của nó) và phương thức readLine().

import java.io.*;

public class ReadLinesDemo {
    public static void main(String[] args) {
        String fileName = "example.txt";

        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            String line;
            int lineNumber = 1;
            while ((line = reader.readLine()) != null) {
                System.out.printf("%3d: %s%n", lineNumber, line);
                lineNumber++;
            }
        } catch (IOException e) {
            System.out.println("Lỗi khi đọc tệp: " + e.getMessage());
        }
    }
}

Ở đây chúng ta mở tệp để đọc và dùng readLine() để đọc từng dòng cho đến khi gặp cuối tệp (null). Sau đó in mỗi dòng kèm số thứ tự.

Tóm tắt về try-with-resources

Bạn thấy cấu trúc try (...) { ... }? Đó là try-with-resources. Nó đảm bảo tệp sẽ được đóng ngay cả khi có lỗi giữa chừng. Bạn không cần gọi close() thủ công: việc đóng sẽ diễn ra tự động, kể cả khi có catch/finally.

Vì sao cần BufferedReader?

BufferedReader không đọc từng ký tự riêng lẻ mà đọc theo khối (thường khoảng 8192 byte), giúp tăng tốc xử lý tệp. Ngoài ra, nó có phương thức tiện lợi readLine(), trả về một chuỗi đến trước ký tự xuống dòng.

Chọn kích thước bộ đệm nào?

Thông thường BufferedReader dùng bộ đệm kích thước 8192 byte (8 KB) — đủ cho hầu hết bài toán. Nếu bạn đọc các dòng rất dài (ví dụ, khoảng 100_000 ký tự), có thể tăng bộ đệm:

BufferedReader reader = new BufferedReader(new FileReader(fileName), 65536); // Bộ đệm 64 KB

Nhưng với các tác vụ thông thường, kích thước mặc định là hoàn hảo.

2. Đọc toàn bộ tệp: Files.readAllLines và Files.readString

Nếu tệp nhỏ (ví dụ, tới 1020 MB), có thể nạp toàn bộ. Ví dụ, khi cần nhanh chóng lấy danh sách tất cả các dòng, phân tích văn bản hoặc gửi qua mạng.

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

Từ Java 7 có lớp tiện lợi Files với rất nhiều phương thức hữu ích.

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

public class ReadAllLinesDemo {
    public static void main(String[] args) {
        Path path = Path.of("example.txt");

        try {
            List<String> lines = Files.readAllLines(path);
            for (int i = 0; i < lines.size(); i++) {
                System.out.printf("%3d: %s%n", i + 1, lines.get(i));
            }
        } catch (IOException e) {
            System.out.println("Lỗi khi đọc tệp: " + e.getMessage());
        }
    }
}

Ở đây phương thức Files.readAllLines(path) trả về danh sách dòng (List<String>). Bạn có thể làm việc với danh sách này như một collection thông thường: tìm kiếm, sắp xếp, lọc.

Còn hiện đại hơn: Files.readString (Java 11+)

Nếu bạn cần lấy toàn bộ tệp thành một chuỗi (ví dụ, để tìm kiếm chuỗi con hoặc truyền vào JSON), hãy dùng Files.readString:

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

public class ReadStringDemo {
    public static void main(String[] args) {
        Path path = Path.of("example.txt");

        try {
            String content = Files.readString(path);
            System.out.println("Nội dung tệp:");
            System.out.println(content);
        } catch (IOException e) {
            System.out.println("Lỗi khi đọc tệp: " + e.getMessage());
        }
    }
}

Còn mã hóa thì sao?

Mặc định, các phương thức Files.readAllLinesFiles.readString dùng mã hóa mặc định của hệ thống. Nếu tệp được ghi bằng mã hóa khác (ví dụ, Windows-1251), hãy chỉ định rõ ràng:

List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
String content = Files.readString(path, StandardCharsets.UTF_8);

3. So sánh các cách tiếp cận: khi nào dùng cách nào

Cách Khi dùng Ưu điểm Nhược điểm
BufferedReader.readLine()
Tệp lớn, xử lý theo dòng Tiết kiệm bộ nhớ, linh hoạt Chỉ đọc theo dòng
Files.readAllLines()
Tệp nhỏ và trung bình Có ngay danh sách dòng, đơn giản Với tệp lớn — OutOfMemory
Files.readString()
Tệp nhỏ, cần toàn bộ văn bản Toàn bộ nội dung thành một chuỗi Không có phân tách theo dòng

Khuyến nghị:
— Nếu tệp nhỏ — hãy dùng Files.readAllLines hoặc Files.readString.
— Nếu tệp lớn hoặc bạn không biết kích thước — hãy dùng BufferedReader.readLine().

4. Bài toán thực tế: ví dụ và phân tích

Ví dụ 1. Đếm số dòng trong tệp lớn

Giả sử cần biết có bao nhiêu dòng trong một tệp rất lớn (ví dụ, log của server).

import java.io.*;

public class LineCount {
    public static void main(String[] args) {
        String fileName = "biglog.txt";
        int lineCount = 0;

        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            while (reader.readLine() != null) {
                lineCount++;
            }
            System.out.println("Tổng số dòng trong tệp: " + lineCount);
        } catch (IOException e) {
            System.out.println("Lỗi khi đọc tệp: " + e.getMessage());
        }
    }
}

Ví dụ 2. Tìm dòng theo nội dung

Tìm tất cả các dòng có chứa từ “lỗi” (không phân biệt hoa/thường):

import java.io.*;

public class FindErrorLines {
    public static void main(String[] args) {
        String fileName = "biglog.txt";
        String keyword = "lỗi";

        try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
            String line;
            int lineNumber = 1;
            while ((line = reader.readLine()) != null) {
                if (line.toLowerCase().contains(keyword)) {
                    System.out.printf("%3d: %s%n", lineNumber, line);
                }
                lineNumber++;
            }
        } catch (IOException e) {
            System.out.println("Lỗi khi đọc tệp: " + e.getMessage());
        }
    }
}

Ví dụ 3. Nạp cấu hình từ tệp nhỏ

Tệp config.txt:

host=localhost
port=8080
mode=dev

Đọc toàn bộ rồi tách thành các cặp khóa-giá trị:

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

public class ConfigLoader {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("config.txt");
        List<String> lines = Files.readAllLines(path);

        Map<String, String> config = new HashMap<>();
        for (String line : lines) {
            if (line.trim().isEmpty() || line.startsWith("#")) continue; // bỏ qua dòng trống và dòng chú thích
            String[] parts = line.split("=", 2);
            if (parts.length == 2) {
                config.put(parts[0].trim(), parts[1].trim());
            }
        }

        System.out.println("Cấu hình đã tải: " + config);
    }
}

5. Lỗi thường gặp khi đọc tệp văn bản

Lỗi #1: Cố đọc tệp nhị phân như tệp văn bản. Nếu bạn mở ảnh hoặc tệp nén bằng BufferedReader hoặc Files.readAllLines, bạn sẽ nhận các ký tự vô nghĩa và có nguy cơ OutOfMemoryError. Với tệp nhị phân, hãy dùng InputStream!

Lỗi #2: Không xử lý ngoại lệ. Tệp có thể bị xóa, di chuyển, khóa. Luôn bao việc đọc trong try-catch và thông báo cho người dùng về sự cố.

Lỗi #3: Bỏ qua mã hóa. Nếu bạn có văn bản bằng tiếng Nga nhưng đọc tệp mà không chỉ định mã hóa, bạn có thể nhận “?????”. Hãy dùng StandardCharsets.UTF_8 hoặc mã hóa phù hợp.

Lỗi #4: Quên đóng stream. Nếu không đóng tệp, nó có thể bị khóa cho đến khi chương trình kết thúc. Luôn dùng try-with-resources.

Lỗi #5: Dùng read() thay vì readLine() cho tệp văn bản. Phương thức read() đọc từng ký tự — chậm và bất tiện cho chuỗi. Với văn bản, hãy dùng readLine() hoặc các phương thức của lớp Files.

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