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
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:
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
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ềucatchđược xét từ trên xuống, loại cụ thể phải đứng trước loại tổng quát (ở đâyNumberFormatExceptionlà lớp con củaIllegalArgumentException).- 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
Đ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…"]| Checked | Unchecked (RuntimeException) | |
|---|---|---|
| Trình biên dịch | Bắt buộc catch hoặc khai báo throws | Không bắt buộc |
| Ý nghĩa | Tình huống bên ngoài, có thể phục hồi: file không tồn tại, mạng lỗi | Thườ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ếp | Sử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
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ý:
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:
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_8khi đọ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ệ:
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
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ên | Trá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ùng | Nuố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ý được | Chỉ 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
- Viết
int nhapSoNguyen(String loiNhac)lặp lại tới khi người dùng nhập đúng số nguyên. - Thêm vào lớp
TaiKhoan(bài 6) ngoại lệ riêngSoTienKhongHopLeExceptionvàVuotHanMucException. - Đọc file danh sách sinh viên
ten,diem(có dòng lỗi), ghi ra filexep-loai.csvvới cột xếp loại; in số dòng bị bỏ qua. - Viết chương trình sao lưu: sao chép mọi file
.txttrong một thư mục sang thư mụcbackup/(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;finallyluô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). throwném,throwskhai 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ệ).
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.