Bỏ qua điều hướng
EN
Tất cả bài viết

Java cơ bản #10: xử lý ngoại lệ và đọc ghi file với java.nio.file

Khi chương trình gặp lỗi: đọc stack trace, try/catch/finally, checked và unchecked exception, throw và throws, tự định nghĩa ngoại lệ nghiệp vụ, try-with-resources tự đóng tài nguyên; đọc ghi file văn bản và CSV bằng Files, Path, UTF-8 — kèm các thói quen xử lý lỗi nên tránh.

12 phút đọc

Ôn tập bài nàyThẻ ghi nhớ & câu hỏi ôn tập7 thẻ ghi nhớ · 6 câu trắc nghiệm
Mục lục

Người dùng nhập chữ vào ô số tuổi. File cấu hình bị xoá. Mạng mất giữa chừng. Chương trình thật luôn gặp tình huống bất thường, và cách nó phản ứng phân biệt phần mềm chuyên nghiệp với phần mềm "thỉnh thoảng sập". Java xử lý các tình huống này bằng ngoại lệ (exception).

Bài này dạy cách đọc, bắt, ném và tự định nghĩa ngoại lệ, rồi áp dụng ngay vào việc đọc ghi file — nơi lỗi xảy ra thường xuyên nhất.

Ngoại lệ là gì

Khi có lỗi lúc chạy, Java ném (throw) một đối tượng ngoại lệ. Nếu không ai bắt (catch), chương trình dừng và in stack trace:

stack-trace
Exception in thread "main" java.lang.NumberFormatException: For input string: "hai mươi"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67)
	at java.base/java.lang.Integer.parseInt(Integer.java:565)
	at DangKy.docTuoi(DangKy.java:4)
	at DangKy.main(DangKy.java:9)

Cách đọc: dòng đầu là loại lỗi và thông điệp; các dòng at là chuỗi lời gọi, mới nhất ở trên. Bỏ qua các dòng của thư viện (java.base/...), tìm dòng đầu tiên thuộc code của bạn — ở đây DangKy.java:4 — đó là chỗ cần xem.

try / catch / finally

TryCatch.java
int docTuoi(String nhap) {
    try {
        int tuoi = Integer.parseInt(nhap.trim());
        if (tuoi < 0 || tuoi > 150) {
            throw new IllegalArgumentException("Tuổi phải từ 0 tới 150: " + tuoi);
        }
        return tuoi;
    } catch (NumberFormatException e) {
        IO.println("Không phải số: '" + nhap + "'");
        return -1;
    } catch (IllegalArgumentException e) {
        IO.println(e.getMessage());
        return -1;
    } finally {
        IO.println("(đã xử lý '" + nhap + "')");    // LUÔN chạy, kể cả khi đã return
    }
}
 
void main() {
    for (String s : new String[]{"25", "hai mươi", "200"}) {
        IO.println("→ " + docTuoi(s));
    }
}
  • try: đoạn code có thể lỗi.
  • catch (Loai e): xử lý một loại ngoại lệ. Nhiều catch được xét từ trên xuống, loại cụ thể phải đứng trước loại tổng quát (ở đây NumberFormatException là lớp con của IllegalArgumentException).
  • Gộp nhiều loại xử lý giống nhau: catch (IOException | SQLException e).
  • finally: luôn chạy — trước đây dùng để đóng file, kết nối; nay thường thay bằng try-with-resources.

Cây ngoại lệ: checked và unchecked

Phân cấp Throwable

Đang vẽ sơ đồ…

flowchart TB
  T[Throwable] --> E["Error — lỗi nghiêm trọng của JVM<br/>(OutOfMemoryError, StackOverflowError)<br/>không nên bắt"]
  T --> X[Exception]
  X --> C["Checked: IOException, SQLException…<br/>BẮT BUỘC xử lý hoặc khai báo throws"]
  X --> R["RuntimeException (unchecked)<br/>NullPointerException, IllegalArgumentException,<br/>IndexOutOfBoundsException, NumberFormatException…"]
CheckedUnchecked (RuntimeException)
Trình biên dịchBắt buộc catch hoặc khai báo throwsKhông bắt buộc
Ý nghĩaTình huống bên ngoài, có thể phục hồi: file không tồn tại, mạng lỗiThường là lỗi lập trình: null, sai chỉ số, tham số không hợp lệ
Cách xử lýBắt và xử lý có ý nghĩa, hoặc ném tiếpSửa code cho đúng, đừng bọc try/catch để che

Ví dụ: NullPointerException hay ArrayIndexOutOfBoundsException nghĩa là code sai — sửa code chứ không bắt ngoại lệ.

throw và throws

ThrowThrows.java
import java.io.IOException;
 
// Kiểm tra tham số đầu vào: ném unchecked exception với thông điệp rõ ràng
long chiaDeu(long tongTien, int soNguoi) {
    if (soNguoi <= 0) throw new IllegalArgumentException("Số người phải dương, nhận được " + soNguoi);
    return tongTien / soNguoi;
}
 
// Phương thức có thể gặp checked exception mà không tự xử lý được → khai báo throws, để nơi gọi quyết định
String taiCauHinh(String ten) throws IOException {
    if (!ten.endsWith(".properties")) throw new IOException("Không đọc được file " + ten);
    return "port=8080";
}
 
void main() {
    IO.println(chiaDeu(900_000, 4));
    try {
        IO.println(taiCauHinh("app.properties"));
        IO.println(taiCauHinh("app.txt"));
    } catch (IOException e) {
        IO.println("Lỗi cấu hình: " + e.getMessage() + " → dùng cấu hình mặc định");
    }
    try {
        chiaDeu(100, 0);
    } catch (IllegalArgumentException e) {
        IO.println("Bắt được: " + e.getMessage());
    }
}

throw (không có s) ném một ngoại lệ; throws (có s) khai báo trong chữ ký phương thức rằng nó có thể ném checked exception.

Ngoại lệ tự định nghĩa

Lỗi nghiệp vụ nên có kiểu riêng, mang đủ thông tin để xử lý:

NgoaiLeRieng.java
class SoDuKhongDuException extends RuntimeException {
    private final long thieu;
 
    SoDuKhongDuException(long soDu, long canRut) {
        super("Số dư %,d không đủ để rút %,d".formatted(soDu, canRut));
        this.thieu = canRut - soDu;
    }
 
    long getThieu() { return thieu; }
}
 
class TaiKhoan {
    private long soDu;
    TaiKhoan(long soDu) { this.soDu = soDu; }
 
    void rut(long soTien) {
        if (soTien > soDu) throw new SoDuKhongDuException(soDu, soTien);
        soDu -= soTien;
    }
    long getSoDu() { return soDu; }
}
 
void main() {
    var tk = new TaiKhoan(1_000_000);
    try {
        tk.rut(300_000);
        tk.rut(900_000);
    } catch (SoDuKhongDuException e) {
        IO.println(e.getMessage() + " — cần nạp thêm %,d".formatted(e.getThieu()));
    }
    IO.println("Số dư: " + tk.getSoDu());
}

Kế thừa RuntimeException (unchecked) là lựa chọn phổ biến trong code hiện đại; chọn checked khi muốn bắt buộc nơi gọi phải xử lý. Khi bọc một ngoại lệ khác, luôn truyền nguyên nhân gốc: throw new DichVuException("Không lưu được đơn", e); — stack trace sẽ có thêm phần Caused by: giúp gỡ lỗi.

Đọc ghi file với java.nio.file

Lớp Files và Path (Java 7+, bổ sung nhiều ở Java 11) là cách hiện đại và gọn nhất để làm việc với file:

DocGhiFile.java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
 
void main() throws IOException {
    Path thuMuc = Path.of("du-lieu");
    Files.createDirectories(thuMuc);                        // tạo nếu chưa có, không lỗi nếu đã có
    Path file = thuMuc.resolve("ghi-chu.txt");               // du-lieu/ghi-chu.txt — đúng dấu / hay \ theo hệ điều hành
 
    Files.writeString(file, "Dòng 1: học Java\n", StandardCharsets.UTF_8);    // ghi mới (đè nếu đã có)
    Files.writeString(file, "Dòng 2: luyện tập\n", StandardCharsets.UTF_8, StandardOpenOption.APPEND);
    Files.write(file, List.of("Dòng 3", "Dòng 4"), StandardCharsets.UTF_8, StandardOpenOption.APPEND);
 
    String toanBo = Files.readString(file, StandardCharsets.UTF_8);
    IO.print(toanBo);
 
    List<String> cacDong = Files.readAllLines(file, StandardCharsets.UTF_8);
    IO.println("Số dòng: " + cacDong.size() + ", dòng cuối: " + cacDong.getLast());
    IO.println("Tồn tại: " + Files.exists(file) + ", kích thước: " + Files.size(file) + " byte");
 
    try {
        Files.readString(Path.of("khong-co.txt"));
    } catch (java.nio.file.NoSuchFileException e) {
        IO.println("Không tìm thấy file: " + e.getMessage());
    }
}

Lưu ý: luôn chỉ rõ UTF_8 khi đọc/ghi chữ tiếng Việt. Từ Java 18 UTF-8 là mặc định, nhưng ghi rõ giúp code đúng trên mọi phiên bản và không phụ thuộc cấu hình máy. Đường dẫn tương đối như du-lieu/ tính từ thư mục đang chạy chương trình — khi chạy từ IDE, đó thường là thư mục gốc của dự án.

try-with-resources

File lớn (hàng GB) không nên đọc hết vào bộ nhớ — đọc từng dòng. Tài nguyên như file, kết nối mạng, kết nối database phải được đóng; try-with-resources tự đóng kể cả khi có ngoại lệ:

DocTungDong.java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
 
void main() throws IOException {
    Path file = Path.of("nhat-ky.log");
    try (BufferedWriter w = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {   // tự đóng khi ra khỏi try
        for (int i = 1; i <= 1000; i++) {
            w.write((i % 100 == 0 ? "ERROR" : "INFO") + " sự kiện " + i);
            w.newLine();
        }
    }
 
    int soLoi = 0;
    try (BufferedReader r = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
        String dong;
        while ((dong = r.readLine()) != null) {          // readLine trả null khi hết file
            if (dong.startsWith("ERROR")) soLoi++;
        }
    }
    IO.println("Số dòng lỗi: " + soLoi);
 
    try (var dongs = Files.lines(file)) {                 // Stream các dòng, cũng phải đóng (bài 11)
        IO.println("Dòng chứa '50': " + dongs.filter(d -> d.contains("50")).count());
    }
}

Mọi lớp cài đặt AutoCloseable đều dùng được trong try (...); khai báo nhiều tài nguyên cách nhau bằng ;, chúng được đóng theo thứ tự ngược.

Ví dụ: đọc và ghi CSV

CsvChiTieu.java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
 
record ChiTieu(String ngay, String danhMuc, long soTien) {
    static ChiTieu tuDongCsv(String dong) {
        String[] cot = dong.split(",");
        if (cot.length != 3) throw new IllegalArgumentException("Sai số cột: " + dong);
        return new ChiTieu(cot[0].trim(), cot[1].trim(), Long.parseLong(cot[2].trim()));
    }
    String sangDongCsv() { return ngay + "," + danhMuc + "," + soTien; }
}
 
void main() throws IOException {
    Path file = Path.of("chi-tieu.csv");
    Files.write(file, List.of(
        "ngay,danh_muc,so_tien",
        "2026-10-01,ăn uống,45000",
        "2026-10-01,đi lại,25000",
        "2026-10-02,ăn uống,abc",                 // dòng lỗi
        "2026-10-03,học tập,350000"), StandardCharsets.UTF_8);
 
    List<ChiTieu> ds = new ArrayList<>();
    List<String> dongLoi = new ArrayList<>();
    List<String> cacDong = Files.readAllLines(file, StandardCharsets.UTF_8);
    for (int i = 1; i < cacDong.size(); i++) {      // bỏ dòng tiêu đề
        try {
            ds.add(ChiTieu.tuDongCsv(cacDong.get(i)));
        } catch (IllegalArgumentException e) {      // NumberFormatException là lớp con
            dongLoi.add("dòng " + (i + 1) + ": " + e.getMessage());
        }
    }
    long tong = 0;
    for (ChiTieu c : ds) tong += c.soTien();
    IO.println("Đọc được " + ds.size() + " khoản, tổng %,d đ".formatted(tong));
    IO.println("Bỏ qua: " + dongLoi);
}

Một dòng lỗi không làm hỏng cả file: bắt lỗi từng dòng, ghi lại để báo người dùng. (CSV thật có dấu phẩy trong dấu ngoặc kép, xuống dòng trong ô… — khi đó dùng thư viện như Apache Commons CSV hoặc OpenCSV.)

Thói quen xử lý lỗi

NênTránh
Bắt loại cụ thể (NoSuchFileException)catch (Exception e) cho mọi thứ
Xử lý có ý nghĩa: thử lại, dùng mặc định, báo người dùngNuốt lỗi: catch (Exception e) {} — lỗi biến mất không dấu vết
Ghi log kèm stack trace khi không xử lý đượcChỉ in e.getMessage() rồi chạy tiếp như không có gì
Kiểm tra tham số sớm, ném IllegalArgumentException với thông điệp rõĐể lỗi lan tới tận sâu rồi mới thành NullPointerException khó hiểu
Dùng if cho tình huống bình thường (map.containsKey, Files.exists)Dùng ngoại lệ để điều khiển luồng thông thường
Truyền nguyên nhân khi bọc ngoại lệ: new XException("...", e)Bọc mà bỏ mất e gốc

Bài tập

  1. Viết int nhapSoNguyen(String loiNhac) lặp lại tới khi người dùng nhập đúng số nguyên.
  2. Thêm vào lớp TaiKhoan (bài 6) ngoại lệ riêng SoTienKhongHopLeException và VuotHanMucException.
  3. Đọc file danh sách sinh viên ten,diem (có dòng lỗi), ghi ra file xep-loai.csv với cột xếp loại; in số dòng bị bỏ qua.
  4. Viết chương trình sao lưu: sao chép mọi file .txt trong một thư mục sang thư mục backup/ (gợi ý: Files.list, Files.copy).

Tổng kết

  • Đọc stack trace: dòng đầu là loại lỗi, tìm dòng at đầu tiên thuộc code của bạn.
  • try/catch/finally; catch cụ thể đứng trước catch tổng quát; finally luôn chạy.
  • Checked (bắt buộc xử lý, tình huống bên ngoài) và unchecked (RuntimeException, thường là lỗi lập trình → sửa code).
  • throw ném, throws khai báo; ngoại lệ nghiệp vụ tự định nghĩa mang đủ thông tin; luôn giữ nguyên nhân gốc.
  • Files + Path đọc ghi file gọn; luôn chỉ rõ UTF-8; file lớn đọc từng dòng; try-with-resources tự đóng tài nguyên.
  • Bài tiếp theo: lambda và Stream API — xử lý dữ liệu kiểu khai báo, gọn hơn vòng lặp.

Nguồn tham khảo: dev.java — "Exceptions", "Java I/O API (java.nio.file)"; tài liệu API Java 25 — java.nio.file.Files; JEP 400: UTF-8 by Default; Bloch — Effective Java, mục 9 (try-with-resources), 69–77 (ngoại lệ).

Đang tải câu hỏi luyện tập…

Bài này thế nào?

Bài viết có hữu ích với bạn?

Đánh giá giúp mình biết nên viết thêm về chủ đề nào.

Đang tải đánh giá…
Chọn số sao từ 1 đến 5

Bình luận

Đăng nhập để bình luận không cần nhập tên và được duyệt nhanh hơn.

0/2000
Đang tải bình luận…

Đọc tiếp

Bài số № 55

Java cơ bản #12: dự án cuối — ứng dụng quản lý chi tiêu với Maven và JUnit

Gom toàn bộ kiến thức vào một dự án thật: cấu trúc Maven và pom.xml, chia lớp model–kho dữ liệu–service–giao diện, lưu file CSV, thống kê bằng Stream, cảnh báo ngân sách, viết kiểm thử JUnit 5, đóng gói file jar chạy được — và lộ trình học tiếp sau Java cơ bản.

19 phút đọc

Bài số № 54

Java cơ bản #11: lambda, tham chiếu phương thức, Stream API và Optional

Lập trình kiểu hàm trong Java: lambda và functional interface (Predicate, Function, Consumer, Supplier), tham chiếu phương thức ::, Stream với filter–map–reduce, sorted, collect, groupingBy, Optional thay cho null — cùng lời khuyên khi nào vòng lặp vẫn tốt hơn stream.

14 phút đọc

Bài số № 52

Java cơ bản #9: Collections — List, Set, Map, Generics và sắp xếp với Comparator

Bộ sưu tập co giãn thay cho mảng: ArrayList, HashSet, HashMap và khi nào dùng loại nào; generics và kiểu bao (Integer), duyệt và xoá an toàn, danh sách bất biến List.of, sắp xếp bằng Comparable/Comparator, Deque làm ngăn xếp và hàng đợi, cùng độ phức tạp của từng thao tác.

16 phút đọc