try-catch-finally
Bài trước: Giới thiệu Exception Handling — Đã học về exception, phân loại exception, và tại sao cần xử lý. Bài này sẽ học cách xử lý exception với try-catch-finally và try-with-resources.
Sau bài này, bạn sẽ:
- Sử dụng try-catch để bắt và xử lý exception an toàn
- Áp dụng multiple catch blocks và multi-catch (Java 7+) đúng thứ tự
- Hiểu finally block luôn chạy và use case chính là cleanup resources
- Sử dụng try-with-resources (Java 7+) để tự động đóng resources
- Tránh các anti-pattern: catch quá generic, swallow exception, nested try-catch sâu
Cú pháp try-catch cơ bản
try-catch là cơ chế chính để xử lý exception trong Java.
try {
// Code có thể throw exception
} catch (ExceptionType e) {
// Xử lý exception
}
Ví dụ đơn giản
public class BasicTryCatch {
public static void main(String[] args) {
try {
int result = 10 / 0; // ArithmeticException
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero!");
System.out.println("Error message: " + e.getMessage());
}
System.out.println("Program continues..."); // ✅ Vẫn chạy
}
}
Output:
Cannot divide by zero!
Error message: / by zero
Program continues...
Exception object methods
try {
String text = null;
text.length();
} catch (NullPointerException e) {
// Các method hữu ích của Exception object
System.out.println(e.getMessage()); // Thông báo ngắn gọn
System.out.println(e.toString()); // Class name + message
e.printStackTrace(); // In ra stack trace đầy đủ
System.out.println(e.getCause()); // Exception gốc (nếu có)
}
getMessage(): Dùng để hiển thị cho userprintStackTrace(): Dùng khi debug (development)getCause(): Dùng khi cần biết exception gốc trong exception chaining
Multiple catch blocks
Một try block có thể có nhiều catch block để xử lý các loại exception khác nhau.
Thứ tự quan trọng: Specific → General
public class MultipleCatch {
public static void main(String[] args) {
try {
String[] names = {"Alice", "Bob"};
String name = names[5]; // ArrayIndexOutOfBoundsException
System.out.println(name.length()); // NullPointerException
} catch (ArrayIndexOutOfBoundsException e) {
// ✅ Xử lý cụ thể cho array index
System.out.println("Invalid array index: " + e.getMessage());
} catch (NullPointerException e) {
// ✅ Xử lý cụ thể cho null reference
System.out.println("Null reference: " + e.getMessage());
} catch (Exception e) {
// ✅ Catch-all cho các exception khác
System.out.println("Unexpected error: " + e.getMessage());
}
}
}
// ❌ SAI: General exception trước
try {
// code
} catch (Exception e) { // Catch tất cả
// handle
} catch (NullPointerException e) { // ❌ UNREACHABLE CODE - Compile error!
// never executed
}
// ✅ ĐÚNG: Specific exception trước
try {
// code
} catch (NullPointerException e) { // ✅ Catch cụ thể trước
// handle
} catch (Exception e) { // ✅ Catch chung sau
// handle
}
Compiler sẽ báo lỗi nếu catch block sau không bao giờ được thực thi.
Ví dụ thực tế: Parse user input
public class UserInputParser {
public static void parseAndCalculate(String[] args) {
try {
// args[0] = số thứ nhất
// args[1] = toán tử (+, -, *, /)
// args[2] = số thứ hai
int num1 = Integer.parseInt(args[0]); // NumberFormatException
String operator = args[1];
int num2 = Integer.parseInt(args[2]); // NumberFormatException
int result = 0;
switch (operator) {
case "+": result = num1 + num2; break;
case "-": result = num1 - num2; break;
case "*": result = num1 * num2; break;
case "/": result = num1 / num2; break; // ArithmeticException
default: throw new IllegalArgumentException("Invalid operator");
}
System.out.println("Result: " + result);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Usage: java Calculator <num1> <operator> <num2>");
} catch (NumberFormatException e) {
System.out.println("Numbers must be valid integers");
} catch (ArithmeticException e) {
System.out.println("Cannot divide by zero");
} catch (IllegalArgumentException e) {
System.out.println("Operator must be +, -, *, or /");
} catch (Exception e) {
System.out.println("Unexpected error: " + e.getMessage());
e.printStackTrace();
}
}
}
Multi-catch (Java 7+)
Tưởng tượng bạn đang chơi bóng — thay vì dùng 2 tay bắt từng loại bóng (catch riêng cho mỗi exception), Java 7 cho phép bạn dùng 1 tay bắt nhiều loại bóng cùng lúc, miễn là cách xử lý giống nhau.
Từ Java 7, có thể catch nhiều exception types trong một catch block bằng |.
Cú pháp
try {
// code
} catch (IOException | SQLException e) {
// Xử lý chung cho cả IOException và SQLException
System.out.println("I/O or Database error: " + e.getMessage());
}
So sánh: Trước và sau Java 7
// ❌ Trước Java 7: Code lặp lại
try {
// code
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
logError(e);
} catch (SQLException e) {
System.out.println("Error: " + e.getMessage()); // Code trùng lặp!
logError(e);
}
// ✅ Java 7+: Gọn hơn
try {
// code
} catch (IOException | SQLException e) {
System.out.println("Error: " + e.getMessage());
logError(e);
}
- Khi xử lý giống nhau cho nhiều exception types
- Khi muốn code gọn gàng hơn
- Lưu ý: Exception types phải không có quan hệ cha-con
Lưu ý quan trọng
// ❌ KHÔNG HỢP LỆ: Exception có quan hệ cha-con
try {
// code
} catch (Exception | IOException e) { // ❌ Compile error!
// IOException là subclass của Exception
}
// ✅ HỢP LỆ: Exception không liên quan
try {
// code
} catch (IOException | SQLException e) { // ✅ OK
// Không có quan hệ cha-con
}
finally block
finally block luôn luôn được thực thi, bất kể có exception hay không, ngay cả khi có return trong try/catch.
Cú pháp
try {
// Code có thể throw exception
} catch (Exception e) {
// Xử lý exception
} finally {
// Code luôn chạy - thường dùng để cleanup resources
}
Ví dụ: finally luôn chạy
public class FinallyExample {
public static void main(String[] args) {
System.out.println(testFinally()); // Output: 10
}
public static int testFinally() {
try {
System.out.println("Try block");
return 5; // Sẽ return 5 phải không?
} catch (Exception e) {
System.out.println("Catch block");
return 10;
} finally {
System.out.println("Finally block - Always executed!");
// ⚠️ Nếu return ở đây, sẽ override return ở try/catch
return 10; // Value cuối cùng được return
}
}
}
Output:
Try block
Finally block - Always executed!
10
public static int dangerousFinally() {
try {
return 1;
} finally {
return 2; // ❌ BAD PRACTICE: Override return value
}
// Luôn return 2, không phải 1!
}
Best Practice: Không nên return trong finally block!
Use case chính: Cleanup resources
public class FileHandler {
public static void readFile(String filename) {
FileReader reader = null;
try {
reader = new FileReader(filename);
// Đọc file
int data = reader.read();
System.out.println((char) data);
} catch (IOException e) {
System.out.println("Error reading file: " + e.getMessage());
} finally {
// ✅ Luôn đóng file, ngay cả khi có exception
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("Error closing file: " + e.getMessage());
}
}
}
}
}
try-with-resources (Java 7+)
try-with-resources tự động đóng resources implement AutoCloseable interface, code gọn hơn và an toàn hơn.
Cú pháp
try (ResourceType resource = new ResourceType()) {
// Sử dụng resource
} catch (Exception e) {
// Xử lý exception
}
// Resource tự động được close, không cần finally!
So sánh: finally vs try-with-resources
// ❌ Cách cũ: Dùng finally (verbose)
public static void oldWay() {
FileReader reader = null;
try {
reader = new FileReader("data.txt");
// read data
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close(); // ⚠️ Phải try-catch lồng nhau
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// ✅ Cách mới: try-with-resources (clean)
public static void newWay() {
try (FileReader reader = new FileReader("data.txt")) {
// read data
} catch (IOException e) {
e.printStackTrace();
}
// reader tự động close!
}
Multiple resources
public static void copyFile(String src, String dest) {
// Khai báo nhiều resources, phân cách bằng ;
try (FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
System.out.println("File copied successfully!");
} catch (IOException e) {
System.out.println("Error copying file: " + e.getMessage());
}
// Cả in và out đều tự động close theo thứ tự ngược lại (out trước, in sau)
}
AutoCloseable interface
// Custom class có thể dùng trong try-with-resources
public class DatabaseConnection implements AutoCloseable {
private String connectionUrl;
public DatabaseConnection(String url) {
this.connectionUrl = url;
System.out.println("Connection opened: " + url);
}
public void executeQuery(String sql) {
System.out.println("Executing: " + sql);
}
@Override
public void close() {
// Method này tự động được gọi khi thoát try block
System.out.println("Connection closed: " + connectionUrl);
}
}
// Sử dụng
try (DatabaseConnection conn = new DatabaseConnection("jdbc:mysql://localhost")) {
conn.executeQuery("SELECT * FROM users");
// Exception xảy ra cũng được close
} catch (Exception e) {
e.printStackTrace();
}
// Output:
// Connection opened: jdbc:mysql://localhost
// Executing: SELECT * FROM users
// Connection closed: jdbc:mysql://localhost
- Code ngắn gọn hơn: Không cần finally block
- An toàn hơn: Đảm bảo resources luôn được close
- Exception handling tốt hơn: Suppressed exceptions được track
- Best practice: Luôn dùng cho resources (File, Connection, Stream, etc.)
Nested try-catch
Có thể lồng try-catch bên trong nhau khi cần xử lý exception ở nhiều cấp độ.
Ví dụ
public class NestedTryCatch {
public static void main(String[] args) {
// Outer try-catch
try {
System.out.println("Outer try block");
try {
// Inner try-catch
System.out.println("Inner try block");
int result = 10 / 0; // ArithmeticException
} catch (ArithmeticException e) {
System.out.println("Inner catch: " + e.getMessage());
throw new RuntimeException("Re-throwing from inner catch");
}
} catch (RuntimeException e) {
System.out.println("Outer catch: " + e.getMessage());
}
}
}
Output:
Outer try block
Inner try block
Inner catch: / by zero
Outer catch: Re-throwing from inner catch
Nested try-catch làm code khó đọc. Hãy cân nhắc refactor thành methods riêng:
// ❌ Khó đọc
try {
try {
try {
// deep nesting
} catch (Exception e) { }
} catch (Exception e) { }
} catch (Exception e) { }
// ✅ Dễ đọc hơn
public void method1() { /* handle exception 1 */ }
public void method2() { /* handle exception 2 */ }
public void method3() { /* handle exception 3 */ }
Ví dụ thực tế: Đọc và parse file
import java.io.*;
import java.util.*;
public class FileParser {
/**
* Đọc file CSV và parse thành list of maps
* File format: name,age,email
*/
public static List<Map<String, String>> parseCSV(String filename) {
List<Map<String, String>> records = new ArrayList<>();
// ✅ try-with-resources cho multiple resources
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
// Đọc header
String headerLine = reader.readLine();
if (headerLine == null) {
throw new IllegalArgumentException("File is empty");
}
String[] headers = headerLine.split(",");
// Đọc data
String line;
int lineNumber = 1;
while ((line = reader.readLine()) != null) {
lineNumber++;
try {
String[] values = line.split(",");
if (values.length != headers.length) {
throw new IllegalArgumentException(
"Line " + lineNumber + ": Column count mismatch"
);
}
Map<String, String> record = new HashMap<>();
for (int i = 0; i < headers.length; i++) {
record.put(headers[i].trim(), values[i].trim());
}
records.add(record);
} catch (IllegalArgumentException e) {
// Log lỗi nhưng tiếp tục parse dòng tiếp theo
System.err.println("Warning: " + e.getMessage());
}
}
} catch (FileNotFoundException e) {
System.err.println("File not found: " + filename);
throw new RuntimeException("Cannot parse non-existent file", e);
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
throw new RuntimeException("I/O error while parsing", e);
}
return records;
}
public static void main(String[] args) {
try {
List<Map<String, String>> data = parseCSV("users.csv");
System.out.println("Parsed " + data.size() + " records");
data.forEach(System.out::println);
} catch (RuntimeException e) {
System.err.println("Failed to parse file: " + e.getMessage());
e.printStackTrace();
}
}
}
Suppressed Exceptions
Đây là kiến thức hay xuất hiện trong OCP exam. Khi dùng try-with-resources, nếu cả try block lẫn close() đều ném exception, Java sẽ giữ exception từ try block là primary và exception từ close() là suppressed.
Luồng hoạt động của Suppressed Exceptions
Vấn đề: 2 exception cùng lúc
public class SuppressedDemo implements AutoCloseable {
private String name;
public SuppressedDemo(String name) {
this.name = name;
}
public void doWork() {
throw new RuntimeException("Exception từ doWork()");
}
@Override
public void close() {
throw new RuntimeException("Exception từ close() - " + name);
}
}
Demo: Primary vs Suppressed
public class SuppressedExceptionDemo {
public static void main(String[] args) {
try (SuppressedDemo r1 = new SuppressedDemo("Resource-1");
SuppressedDemo r2 = new SuppressedDemo("Resource-2")) {
r1.doWork(); // 💥 Ném exception
} catch (RuntimeException e) {
// Primary exception: từ doWork()
System.out.println("Primary: " + e.getMessage());
// Suppressed exceptions: từ close() của r2 và r1
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Suppressed: " + suppressed.getMessage());
}
}
}
}
Output:
Primary: Exception từ doWork()
Suppressed: Exception từ close() - Resource-2
Suppressed: Exception từ close() - Resource-1
Quy tắc quan trọng
- Primary exception = exception từ try block
- Suppressed exceptions = exceptions từ
close(), được gắn vào primary quaaddSuppressed() - Thứ tự close: Resources đóng theo thứ tự ngược (khai báo sau đóng trước)
- Truy cập suppressed exceptions qua
Throwable.getSuppressed()
Với finally (cách cũ), exception từ close() trong finally sẽ mất exception gốc từ try block:
// ❌ finally: mất exception gốc
try {
throw new RuntimeException("Original"); // MẤT!
} finally {
throw new RuntimeException("From finally"); // Chỉ giữ cái này
}
// ✅ try-with-resources: giữ cả hai
try (MyResource r = new MyResource()) {
throw new RuntimeException("Original"); // Primary
} // close() ném exception → Suppressed
Đây là lý do luôn nên dùng try-with-resources thay vì try-finally!
Try-with-resources cải tiến (Java 9+)
Java 9 cho phép dùng effectively final variables đã khai báo trước đó trong try-with-resources, không cần khai báo lại.
So sánh Java 7 vs Java 9
// Java 7/8: Phải khai báo resource trong try()
FileReader reader = new FileReader("data.txt");
try (FileReader r = reader) { // ❌ Phải tạo biến mới r
// sử dụng r
}
// Java 9+: Dùng trực tiếp effectively final variable
FileReader reader = new FileReader("data.txt");
try (reader) { // ✅ Dùng trực tiếp biến reader
// sử dụng reader
}
Nhiều resources (Java 9+)
BufferedReader br = new BufferedReader(new FileReader("input.txt"));
PrintWriter pw = new PrintWriter(new FileWriter("output.txt"));
// Java 9+: Gọn hơn nhiều
try (br; pw) {
String line;
while ((line = br.readLine()) != null) {
pw.println(line.toUpperCase());
}
}
Biến phải là effectively final — tức là không được gán lại giá trị sau khi khởi tạo. Nếu bạn gán lại giá trị, compiler sẽ báo lỗi.
Edge Cases quan trọng
Exception trong finally block — mất original exception
public static void main(String[] args) {
try {
System.out.println(riskyMethod());
} catch (Exception e) {
// Chỉ nhận được "finally exception", KHÔNG phải "try exception"
System.out.println(e.getMessage()); // "finally exception"
}
}
static int riskyMethod() {
try {
throw new RuntimeException("try exception"); // ← MẤT!
} finally {
throw new RuntimeException("finally exception"); // ← Chỉ giữ cái này
}
}
Khi cả try/catch block và finally đều throw exception, exception từ finally sẽ "nuốt" exception gốc. Đây là lý do không nên throw exception trong finally.
try-finally không có catch — valid pattern
// ✅ Valid: try-finally mà không có catch
// Dùng khi muốn cleanup nhưng để exception propagate lên caller
public void processFile(String name) throws IOException {
FileReader reader = new FileReader(name);
try {
// xử lý file
} finally {
reader.close(); // Luôn đóng, dù có exception hay không
}
}
Pattern này hữu ích khi bạn muốn cleanup resources nhưng không muốn xử lý exception — để caller tự quyết định.
Tóm tắt
| Cấu trúc | Mục đích | Lưu ý |
|---|---|---|
| try-catch | Xử lý exception | Thứ tự catch: specific → general |
| multi-catch | Catch nhiều exception types | Exception không có quan hệ cha-con |
| finally | Code luôn chạy (cleanup) | Không return trong finally |
| try-with-resources | Auto-close resources | Resource phải implement AutoCloseable |
| nested try-catch | Xử lý exception nhiều cấp | Tránh lạm dụng, nên refactor thành methods |
| Suppressed Exceptions | Exception từ close() khi try cũng throw | Dùng getSuppressed() để truy cập |
| Java 9 TWR | Dùng effectively final variable | Gọn hơn, không cần khai báo lại |
Bài tập
-
Bài 1: Viết method
readInt(Scanner scanner)đọc số nguyên từ user. Nếu user nhập không phải số, yêu cầu nhập lại (dùng loop + try-catch). -
Bài 2: Tạo method
divide(int a, int b)chia 2 số. Xử lýArithmeticExceptionkhi chia cho 0. -
Bài 3: Viết chương trình đọc file text và đếm số dòng. Sử dụng try-with-resources.
-
Bài 4: Giải thích output của code sau:
public static int mystery() {
try {
return 1;
} finally {
return 2;
}
}
- Bài 5: Refactor code sau để dùng try-with-resources:
FileReader fr = null;
try {
fr = new FileReader("data.txt");
// read file
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Trong bài tiếp theo, chúng ta sẽ học cách tạo Custom Exceptions và sử dụng throw / throws!