1. Hệ phân cấp ngoại lệ nhập/xuất
Trong Java, khi làm việc với tệp và các tài nguyên bên ngoài khác, bạn gần như luôn phải đối mặt với các ngoại lệ nhập/xuất. Đây là các đối tượng được ném ra (throw) trong trường hợp có sự cố khi đọc hoặc ghi dữ liệu. Ví dụ, nếu tệp không được tìm thấy, không có quyền truy cập hoặc đĩa đột nhiên “mệt”.
Nhân vật chính của bài giảng
Trong Java có cả một hệ phân cấp các ngoại lệ như vậy. Dưới đây là những cái chính:
- IOException — lớp cơ sở cho mọi lỗi nhập/xuất. Nếu có điều gì đó trục trặc với tệp, luồng hoặc mạng — hầu như luôn là “lỗi” của nó (hoặc một trong những lớp con rất nhiều của nó).
- FileNotFoundException — lớp con của IOException, xuất hiện khi bạn cố mở một tệp không tồn tại hoặc đường dẫn được chỉ định sai.
Các lớp dẫn xuất khác:
- EOFException — cũng là lớp con trực tiếp của IOException. Báo hiệu rằng khi đọc, chúng ta đã bất ngờ đến cuối tệp.
- MalformedInputException — kiểu “cháu”: kế thừa từ CharacterCodingException, và bản thân nó kế thừa từ IOException. Lỗi này xảy ra khi tệp không thể được diễn giải đúng theo mã hóa đã chỉ định (ví dụ, mong đợi "UTF-8" nhưng nhận được chuỗi bị hỏng).
- Ngoài ra còn có SocketException, ZipException và các “họ hàng” chuyên biệt khác, mỗi loại có phạm vi trách nhiệm riêng. Càng xuống sâu trong hệ phân cấp, tình huống càng hẹp và đặc thù.
Sơ đồ đơn giản hóa nhỏ:
java.lang.Exception
└── java.io.IOException
├── java.io.FileNotFoundException
├── java.io.EOFException
├── java.nio.charset.MalformedInputException
└── ... (khác)
Thông tin thú vị:
Trong Java, hầu như mọi thao tác với tệp đều yêu cầu khai báo hoặc xử lý IOException — đây là các checked exceptions. Trình biên dịch sẽ không cho bạn quên xử lý lỗi!
2. Khi nào và vì sao các ngoại lệ này phát sinh
Mở một tệp không tồn tại
Tình huống thường gặp nhất — bạn cố mở tệp nhưng nó không tồn tại. Giống như đến bến xe buýt mà chiếc xe đó thậm chí không có trong lịch trình.
FileInputStream fis = new FileInputStream("abracadabra.txt"); // Bùm! FileNotFoundException
Cố ghi vào tệp khi không có quyền
Nếu bạn cố ghi một tệp vào thư mục mà bạn không có quyền, bạn sẽ nhận FileNotFoundException hoặc IOException (tùy tình huống).
FileOutputStream fos = new FileOutputStream("/system/secret.txt"); // Bùm! FileNotFoundException hoặc IOException
Lỗi đọc/ghi do thiết bị lưu trữ bị hỏng
Đôi khi tệp có vẻ như tồn tại, nhưng đĩa bị hỏng, tệp bị chương trình khác khoá hoặc bất chợt mất điện — khi đó bạn sẽ nhận IOException với nhiều thông điệp khác nhau.
Nguyên nhân khác
- Tệp chỉ được mở ở chế độ chỉ đọc, còn bạn lại cố ghi.
- Đường dẫn đến tệp quá dài hoặc chứa ký tự không hợp lệ.
- Tệp đang bị tiến trình khác sử dụng.
- Đĩa đầy.
- Tệp bị tiến trình khác xóa giữa lúc bạn kiểm tra và sử dụng.
3. Xử lý bằng try-catch
Bất cứ khi nào bạn làm việc với tệp, luồng, mạng — hãy dùng try-catch. Nó giống như túi khí an toàn: nếu có sự cố, chương trình sẽ không sập mà có thể phản ứng một cách phù hợp.
Bắt ngoại lệ đúng cách như thế nào?
Trong Java, cần bắt các ngoại lệ cụ thể trước, rồi mới đến các ngoại lệ tổng quát. Nếu đặt catch tổng quát (IOException e) lên trước, thì các ngoại lệ hẹp hơn như FileNotFoundException sẽ không bao giờ được xử lý — luồng điều khiển sẽ không tới chúng.
Cấu trúc đúng:
try {
// Làm việc với tệp
} catch (FileNotFoundException e) {
// Xử lý tình huống "không tìm thấy tệp"
} catch (IOException e) {
// Xử lý các lỗi nhập/xuất khác
}
Vì sao?
Bởi vì FileNotFoundException là trường hợp cụ thể của IOException. Nếu bạn bắt trường hợp tổng quát trước, thì trường hợp cụ thể sẽ không “tới” được khối catch của nó.
Ví dụ mã: xử lý lỗi khi mở tệp
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) {
String filename = "notes.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String line = reader.readLine();
System.out.println("Dòng đầu tiên của tệp: " + line);
reader.close();
} catch (FileNotFoundException e) {
System.out.println("Không tìm thấy tệp: " + filename);
} catch (IOException e) {
System.out.println("Lỗi khi đọc tệp: " + e.getMessage());
}
}
}
Lưu ý:
Ngay cả khi bạn đã kiểm tra rằng tệp tồn tại, hãy luôn để lại try-catch — tệp có thể biến mất bất cứ lúc nào (ví dụ, bị tiến trình khác xóa).
4. Thực hành: viết mã với xử lý lỗi
Tạo các ví dụ đơn giản và khiến chúng phản ứng đúng khi thiếu tệp và các lỗi khác.
Bước 1: Thử mở tệp không tồn tại
import java.io.*;
public class NotesApp {
public static void main(String[] args) {
String filename = "my_notes.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String line;
System.out.println("Ghi chú của bạn:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
} catch (FileNotFoundException e) {
System.out.println("Ôi! Không tìm thấy tệp ghi chú: " + filename);
System.out.println("Gợi ý: hãy tạo tệp hoặc kiểm tra tên.");
} catch (IOException e) {
System.out.println("Đã xảy ra lỗi khi đọc tệp: " + e.getMessage());
}
}
}
Bước 2: Thêm xử lý cho việc ghi tệp
import java.io.*;
public class NotesWriter {
public static void main(String[] args) {
String filename = "my_notes.txt";
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(filename, true)); // append: true
writer.write("Ghi chú mới!\n");
writer.close();
System.out.println("Đã thêm ghi chú thành công!");
} catch (IOException e) {
System.out.println("Lỗi khi ghi vào tệp: " + e.getMessage());
}
}
}
Bước 3: Ví dụ tổng quát với ghi log lỗi
import java.io.*;
public class SafeFileCopier {
public static void main(String[] args) {
String source = "source.txt";
String target = "target.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(source));
BufferedWriter writer = new BufferedWriter(new FileWriter(target));
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
reader.close();
writer.close();
System.out.println("Tệp đã được sao chép thành công!");
} catch (FileNotFoundException e) {
System.out.println("Không tìm thấy tệp: " + e.getMessage());
} catch (IOException e) {
System.out.println("Lỗi I/O: " + e.getMessage());
}
}
}
Mẹo:
Trong ứng dụng thực tế, nên ghi log lỗi không chỉ ra màn hình mà còn vào tệp hoặc nhật ký, để sau này có thể tìm ra chính xác điều gì đã xảy ra.
5. Bảng: các ngoại lệ I/O chính
| Ngoại lệ | Khi nào xảy ra? | Xử lý thế nào? |
|---|---|---|
|
Không tìm thấy tệp, đường dẫn không tồn tại, thiếu quyền | Thông báo cho người dùng, kiểm tra đường dẫn/quyền, nếu cần thì tạo tệp |
|
Kết thúc tệp bất ngờ khi đọc | Thông báo về hư hỏng/thiếu dữ liệu, thử khôi phục một phần |
|
Lỗi I/O chung (đĩa, quyền, khóa) | Kiểm tra chi tiết, kết thúc thao tác an toàn, nếu có thể thì thử lại |
|
Mã hóa hoặc cấu trúc tệp không hợp lệ | Thông báo về hư hỏng, thử mã hóa/nguồn khác |
6. Lỗi thường gặp khi xử lý ngoại lệ I/O
Lỗi số 1: Chỉ bắt Exception tổng quát. Rất cám dỗ khi chỉ viết catch (Exception e), nhưng như vậy bạn sẽ không phân biệt được chính xác điều gì đã xảy ra. Tốt hơn hãy bắt các ngoại lệ cụ thể trước (FileNotFoundException), rồi sau đó — IOException tổng quát.
Lỗi số 2: Không đóng luồng khi có lỗi. Nếu bạn đã mở tệp rồi phát sinh ngoại lệ, luồng có thể bị bỏ quên chưa đóng. Hãy dùng try-with-resources hoặc đóng tài nguyên trong finally.
Lỗi số 3: Bỏ qua thông điệp của ngoại lệ. Đừng chỉ in ra “Lỗi!”, hãy hiển thị chi tiết: e.getMessage(). Điều này giúp hiểu nhanh hơn điều gì đã sai.
Lỗi số 4: Không xử lý FileNotFoundException khi ghi. Nhiều người nghĩ FileNotFoundException chỉ liên quan đến đọc. Thực tế, nó cũng có thể xảy ra khi ghi (đường dẫn không hợp lệ, không có quyền tạo tệp, v.v.).
Lỗi số 5: Không kiểm tra quyền truy cập. Nếu chương trình chạy với quyền hạn chế, nhiều thao tác với tệp có thể thất bại. Luôn tính đến điều này và thông báo cho người dùng.
GO TO FULL VERSION