try, catch, finally
Xem thửTình huống mở đầu
Hôm qua chương trình đọc đơn của quán Cà Phê Gió dừng ngang ở đơn thứ hai vì nhân viên gõ nhầm mocha,x, và 198 đơn sau đó không được đọc. Bạn muốn một đơn hỏng chỉ bị đánh dấu là hỏng, còn các đơn khác vẫn chạy. Bạn bọc code trong try và catch, chạy thử và mọi thứ ổn. Nhưng chủ quán hỏi tiếp: đơn nào lỗi cũng phải ghi lại là đã kiểm tra xong để cuối ca đối chiếu, dù lỗi hay không lỗi. Chép dòng ghi đó vào hai chỗ thì dễ quên một chỗ.
Có cách nào viết đúng một lần mà chạy cả khi đơn tốt lẫn khi đơn xấu không? Hãy giữ câu hỏi này, vì bài hôm nay trả lời nó bằng khối finally.
Ôn nhanh bài trước: ngoại lệ không ai bắt thì chuyện gì xảy ra với các dòng phía sau chỗ ném?
Đáp án: ngoại lệ bỏ dở từng khung của ngăn xếp gọi hàm cho tới main, nên các dòng sau chỗ ném không chạy và chương trình kết thúc với mã thoát 1. Bài này là cách chặn nó lại.
Mục tiêu bài học
Sau bài này, bạn có thể:
- Viết được
tryvàcatchbắt đúng loại ngoại lệ để chương trình đọc tiếp các đơn còn lại. - Dự đoán được thứ tự các dòng chạy qua
try,catchvàfinallytrong ba trường hợp: không lỗi, lỗi cócatchkhớp, và lỗi khôngcatchnào khớp. - Giải thích được vì sao
catchhẹp phải đứng trướccatchrộng, và vì saofinallychạy cả khitryđã gặpreturn. - Phát hiện và sửa được ba lỗi hay gặp:
catchrỗng nuốt lỗi,catchrộng đứng trước, vàtrythiếucatchhoặcfinally.
Bài này đo được bằng các bài tập và năm câu ở phần Tự kiểm tra, mỗi câu ghi rõ nó kiểm tra mục tiêu nào.
Hãy xem video dưới đây để có cái nhìn đầu tiên, sau đó mình đi chậm hơn từng bước ở các phần sau.
Bức tranh lớn (Big Picture)
Trước khi vào chi tiết, hãy nhìn ba con đường một khối try có thể đi.
Cách đọc hình: đọc từ trái sang phải. Ô trắng bên trái là khối try bắt đầu. Ba ô ở giữa là ba kết cục: không lỗi (xanh lá), có lỗi và có catch khớp (vàng), có lỗi nhưng không catch nào khớp (đỏ). Cả ba cùng dẫn vào ô finally. Bên phải, hai kết cục đầu cho chương trình đi tiếp, kết cục thứ ba thì ngoại lệ tiếp tục trèo lên. Mỗi ô đều có nhãn chữ, màu chỉ là tín hiệu phụ.
Ẩn dụ xuyên suốt của bài: catch là cái khay hứng dưới máy pha cà phê, hứng đúng loại sự cố nó được đặt để hứng. finally là việc dọn quầy cuối ca: dù ca êm hay có đổ vỡ, việc dọn quầy vẫn phải làm. Dự án xuyên suốt là chương trình đọc đơn của quán, nay sống sót qua đơn hỏng. Chỗ ẩn dụ không còn đúng: khay hứng thật hứng được mọi thứ rơi xuống, còn catch chỉ hứng đúng loại ngoại lệ ghi trong ngoặc.
Khái niệm cốt lõi
Bài có bốn khái niệm, mỗi khái niệm một hình.
Khái niệm 1: try và catch, chặn lỗi lại
Khối try chứa các dòng có thể lỗi. Khối catch là phần xử lý, đi kèm một loại ngoại lệ, chỉ chạy khi try ném đúng loại đó (hoặc loại con của nó). Khi một dòng trong try ném lỗi, các dòng còn lại của try bị bỏ qua và Java nhảy thẳng tới catch khớp.
Cách đọc hình: hàng trên là các dòng của try với đơn mocha,x: split chạy tốt (xanh lá), parseInt ném lỗi (đỏ), dòng in total bị bỏ qua (xám, mũi tên đứt). Mũi tên đi xuống tới ô vàng là catch khớp. Hàng dưới là catch, rồi finally, rồi đơn kế tiếp, mọi thứ đi tiếp thay vì dừng.
Concept Check 1. Trong process("mocha,x"), khối try có ba dòng: split, parseInt, rồi in total. Dòng nào chạy?
- A. Cả ba dòng, vì lỗi chỉ được ghi lại.
- B. Dòng
splitvàparseIntbắt đầu chạy, dòng in total thì không. - C. Chỉ
split, vìparseIntbị bỏ qua trước khi chạy. - D. Không dòng nào, vì Java dò lỗi trước khi chạy.
Đáp án: B. parseInt đã bắt đầu chạy và chính nó ném lỗi, nên dòng sau nó mới bị bỏ qua. A coi lỗi như ghi chú, sai. C và D nghĩ Java biết trước lỗi, nhưng với NumberFormatException chỉ khi parseInt thật sự chạy mới biết.
Khái niệm 2: thứ tự các catch, và gộp nhiều loại
Một try có thể có nhiều catch. Java xét chúng từ trên xuống và chạy cái đầu tiên khớp, các cái sau bị bỏ qua. Vì loại con cũng khớp với loại cha, catch hẹp phải đứng trước catch rộng. Khi nhiều loại cần cùng một cách xử lý, viết gộp bằng dấu gạch đứng: catch (A | B e).
Cách đọc hình: hàng trên: lỗi ArithmeticException (ô đỏ) đi lần lượt qua hai catch không khớp (xám) rồi gặp catch ArithmeticException khớp (xanh lá). Hàng dưới màu đỏ là thứ tự sai: catch RuntimeException đứng trước đã khớp hết nên catch ArithmeticException phía sau không bao giờ tới lượt, trình biên dịch báo lỗi.
Đây là khái niệm quan trọng nhất của bài vì nó quyết định catch nào làm việc. Mình đã chạy một phiên bản gộp ba loại bằng catch (NumberFormatException | ArrayIndexOutOfBoundsException | ArithmeticException e) và in e ra [Đã chạy, JDK 17.0.15]: mocha,x rejected: java.lang.NumberFormatException: For input string: "x", và hai đơn còn lại cho java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1 và java.lang.ArithmeticException: / by zero. In e cho ra tên loại lỗi kèm thông báo.
Concept Check 2. Một try có catch (RuntimeException e) đứng trước catch (ArithmeticException e). Điều gì xảy ra?
- A. Biên dịch được, và
ArithmeticExceptionluôn rơi vàocatchđầu. - B. Biên dịch được, và Java chọn
catchhẹp nhất bất kể thứ tự. - C. Biên dịch được nhưng
catchthứ hai không bao giờ chạy. - D. Lỗi biên dịch, vì
catchthứ hai chắc chắn không bao giờ tới lượt.
Đáp án: D. Thông báo thật có ở phần Thử phá nó đi. A và C hình dung đúng hành vi nhưng sai chuyện biên dịch: Java không cho phép viết catch không bao giờ chạy được. B bịa ra một quy tắc chọn theo độ hẹp mà Java không có: nó chọn theo thứ tự viết.
Khái niệm 3: finally, việc luôn phải làm
Khối finally chạy sau try và catch trong mọi trường hợp: không lỗi, lỗi đã bắt, lỗi không ai bắt, và cả khi try đã gặp return. Nó là nơi đặt việc dọn dẹp, như đóng quầy hay ghi nhận đã kiểm tra xong.
Cách đọc hình: hàng trên: return 1 trong try đặt giá trị 1 sang một bên, finally in một dòng, rồi phương thức mới trả về 1. Hàng dưới: parseInt ném lỗi, catch chỉ muốn ArithmeticException nên không khớp, finally vẫn in dòng của nó, rồi ngoại lệ đi lên cho catch ngoài xử lý.
Hai đoạn dưới đây trích từ FinallyDemo.java, chạy thật [Đã chạy, JDK 17.0.15]:
static int attempt() {
try {
return 1;
} finally {
// Chạy trước khi lệnh return thật sự hoàn tất
System.out.println("finally runs first");
}
}
static void noMatch() {
try {
Integer.parseInt("x");
} catch (ArithmeticException e) {
System.out.println("not reached");
} finally {
// Không catch nào khớp, finally vẫn chạy rồi ngoại lệ đi tiếp lên trên
System.out.println("finally still runs");
}
}
Trong main, System.out.println(attempt()); rồi noMatch() bọc trong một try có catch (NumberFormatException e) in outer caught kèm e. Kết quả:
finally runs first
1
finally still runs
outer caught java.lang.NumberFormatException: For input string: "x"
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
return 1; trong try | Tính giá trị 1 nhưng chưa rời phương thức | Phải chạy finally trước, nên dòng finally runs first in trước số 1 |
catch (ArithmeticException e) | Chỉ khớp loại ngoại lệ này | parseInt ném NumberFormatException nên không khớp, thân không chạy |
finally { ... } trong noMatch | Chạy dù không catch nào khớp | Việc dọn dẹp không phụ thuộc vào chuyện lỗi có được bắt hay không |
catch (NumberFormatException e) ở main | Bắt ngoại lệ đang trèo lên | Có kế hoạch ở khung ngoài nên chương trình không dừng |
Khái niệm 4: đừng nuốt lỗi trong im lặng
Nuốt lỗi là viết catch mà thân rỗng, nên lỗi biến mất và không ai biết đã có chuyện. Một catch tốt ít nhất phải báo lại điều đã xảy ra, hoặc trả về một giá trị phân biệt được với dữ liệu thật.
static int swallow(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
// Catch rỗng: lỗi biến mất, không ai biết đã có chuyện
}
return 0;
}
Kết quả khi gọi swallow("abc") và swallow("0"), so với một bản report in thông báo và trả về -1 [Đã chạy, JDK 17.0.15]:
swallow abc 0
swallow 0 0
bad number: abc
report abc -1
report 0 0
swallow("abc") và swallow("0") cho cùng kết quả 0, không phân biệt được dữ liệu hỏng với dữ liệu thật bằng 0. Bản report cho -1 và in bad number: abc, nên bạn nhận ra ngay.
Ví dụ đầu tiên (Worked Example) và "Xem từng bước"
Chương trình đọc đơn của quán giờ sống sót qua đơn hỏng, và vẫn in end.
SafeReader.java
public class SafeReader {
public static void main(String[] args) {
String[] lines = {"latte,2", "mocha,x", "tea", "cake,0"};
for (String line : lines) {
process(line);
}
System.out.println("end");
}
static void process(String line) {
try {
String[] parts = line.split(",");
int qty = Integer.parseInt(parts[1]);
// Dòng cuối của try: chỉ chạy khi hai dòng trên không ném lỗi
System.out.println(line + " total " + 300 / qty);
} catch (NumberFormatException e) {
System.out.println(line + " bad number");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println(line + " missing quantity");
} catch (ArithmeticException e) {
System.out.println(line + " zero quantity");
} finally {
// finally chạy ở cả bốn trường hợp, có lỗi hay không
System.out.println("checked " + line);
}
}
}
Predict: hãy dừng lại và đoán trên giấy nháp: với bốn đơn, chương trình in ra những dòng nào, theo thứ tự nào, và dòng end có in không?
Kết quả mong đợi [Đã chạy, JDK 17.0.15]:
latte,2 total 150
checked latte,2
mocha,x bad number
checked mocha,x
tea missing quantity
checked tea
cake,0 zero quantity
checked cake,0
end
Đọc code từng phần:
| Dòng code | Máy làm gì | Vì sao viết như vậy |
|---|---|---|
for (String line : lines) { process(line); } | Gọi process cho từng đơn | Vòng lặp không biết lỗi xảy ra, vì process đã tự xử lý |
Integer.parseInt(parts[1]) | Đổi phần số lượng thành số | Có thể ném NumberFormatException hoặc, nếu parts[1] không tồn tại, ArrayIndexOutOfBoundsException |
300 / qty | Chia 300 cho số lượng | Số lượng 0 ném ArithmeticException |
catch (NumberFormatException e) | In bad number khi chữ không phải số | Mỗi loại lỗi một thông điệp riêng cho dễ hiểu |
catch (ArrayIndexOutOfBoundsException e) | In missing quantity khi thiếu dấu phẩy | Ba catch hẹp, không cái nào là cha của cái kia nên thứ tự tự do |
finally { ... } | In checked ở mọi đơn | Viết một lần, chạy cho cả bốn đơn dù có lỗi hay không |
System.out.println("end"); | In sau vòng lặp | Chạy được vì mọi lỗi đã bị chặn lại ở process |
Bây giờ xem từng bước máy chạy cho đơn mocha,x:
Đang tải Animation…
Bảng trace (bản chữ tương đương của animation, [Suy luận] bằng cách đi từng dòng và khớp với kết quả chạy thật):
| Đơn | Dòng của try chạy | Lỗi | catch chạy | finally | In ra |
|---|---|---|---|---|---|
latte,2 | cả ba dòng | không | không | có | latte,2 total 150, checked latte,2 |
mocha,x | split, parseInt | NumberFormatException | NumberFormatException | có | mocha,x bad number, checked mocha,x |
tea | split, parseInt | ArrayIndexOutOfBoundsException | ArrayIndexOutOfBoundsException | có | tea missing quantity, checked tea |
cake,0 | cả ba dòng | ArithmeticException | ArithmeticException | có | cake,0 zero quantity, checked cake,0 |
Bạn vừa làm được: đọc một chương trình có try, catch, finally và nói đúng dòng nào chạy, dòng nào bị bỏ qua cho từng đơn.
Concept Check xen kẽ
Chọn đáp án của bạn trước, rồi đọc phần đáp án ngay bên dưới.
Concept Check 3. attempt() có return 1; trong try và finally in một dòng. Chuyện gì xảy ra?
- A. Dòng của
finallyin trước, rồi phương thức trả về 1. - B. Phương thức trả về 1 trước,
finallyin sau khi nơi gọi đã nhận 1. - C.
finallybị bỏ qua vìreturnkết thúc phương thức ngay. - D. Lỗi biên dịch, vì
trycóreturnthì không được cófinally.
Đáp án: A. B và C nghĩ return thoát ngay, nhưng Java luôn chạy finally trước khi rời khối try. D bịa ra một cấm đoán không có.
Concept Check 4. swallow("abc") và swallow("0") đều trả về 0. Điều đó cho thấy gì?
- A.
Integer.parseIntluôn trả về 0 khi gặp chữ. - B.
catchbắt thiếu loại ngoại lệ nên lỗi vẫn tiếp tục lên trên. - C. Catch rỗng làm lỗi biến mất, nên không phân biệt được dữ liệu hỏng với dữ liệu thật.
- D. Hàm này chạy sai vì
return 0nằm ngoàitry.
Đáp án: C. A sai vì parseInt ném lỗi chứ không trả 0. B sai vì NumberFormatException đã được bắt đúng. D sai vì return 0 ở ngoài chỉ chạy khi catch kết thúc bình thường, đó là thiết kế có chủ đích nhưng làm mất thông tin.
Concept Check 5. Trong SafeReader, một đơn có dạng latte,2,5 (ba phần tách bởi dấu phẩy). Chuyện gì xảy ra với đơn này?
- A. In
latte,2,5 bad number, vì2,5không phải một số. - B. In
latte,2,5 total 150, rồichecked latte,2,5, vì chỉ phần thứ hai được dùng. - C. In
latte,2,5 missing quantity, vì mảng có quá nhiều phần tử. - D. In
latte,2,5 zero quantity, vì phần thứ ba bị đọc thành 0.
Đáp án: B. split(",") cho mảng latte, 2, 5 nhưng code chỉ đọc parts[1] là 2, nên mọi thứ chạy trơn tru và finally vẫn in checked. A nhầm split giữ nguyên chuỗi 2,5. C chỉ đúng khi mảng quá ngắn, không phải quá dài. D bịa ra việc phần thừa bị đọc thành 0.
Làm cùng mình (Guided Practice)
Bạn sẽ viết hàm serve của quầy: mở quầy, đọc độ dài tên đồ uống, và luôn đóng quầy. Hỗ trợ giảm dần: bước 1 gần như có sẵn, bước 3 chỉ còn đề. Tạo file Practice.java có main gọi serve("latte"); rồi serve(null);.
Bước 1: try.
static void serve(String drink) {
System.out.println("open");
try {
// TODO 1: in ra độ dài của drink
} catch (NullPointerException e) {
// TODO 2: in ra no drink
}
}
Trong main gọi serve("latte");. Mới làm chỗ trống số 1 thôi, để catch trống. Chạy xong bạn sẽ thấy: open rồi 5. Gợi ý: drink.length() cho độ dài chuỗi.
Bước 2: catch. Làm chỗ trống số 2, rồi trong main gọi thêm serve(null);. Chạy xong bạn sẽ thấy: open, 5, open, no drink. Gợi ý: drink.length() trên null ném NullPointerException, đúng loại mà catch đang chờ.
Vẽ ra giấy: vẽ đường đi của serve(null) qua ba khối try, catch, finally và gạch những dòng được chạy. Đối chiếu: dòng open chạy, dòng in độ dài chạy nhưng ném lỗi, catch chạy và in no drink; dòng sau lỗi trong try không có vì lỗi nằm ngay dòng đó.
Bước 3: finally. Bạn tự quyết cách viết: thêm một khối finally in close, để cả hai lần gọi đều in close ở cuối. Chạy xong bạn sẽ thấy sáu dòng: open, 5, close, open, no drink, close.
Bước 4: phá thử. Đặt catch (RuntimeException e) lên trước catch (NullPointerException e) và chạy, đọc thông báo. Nó có dạng giống dòng đầu của bảng lỗi ở phần Thử phá nó đi. Đưa thứ tự về như cũ. Lời giải ở cuối bài.
Bạn vừa làm được: viết đủ bộ ba try, catch, finally và tự vẽ được đường đi của lệnh gọi lỗi. Toàn bộ mã nguồn của bài nằm trong thư mục 08-ngoai-le/02-try-catch-finally của kho GitHub, nhưng bạn không cần mở nó để học.
Thử phá nó đi (Lỗi thường gặp)
Bốn lỗi dưới đây là những lỗi người mới gặp nhiều nhất. Ba dòng đầu là lỗi biên dịch chạy thật bằng java File.java, dòng cuối là chuyện xảy ra khi chạy [Đã chạy, JDK 17.0.15; chữ thông báo có thể khác ở phiên bản Java khác].
| Code lỗi | Thông báo lỗi | Nguyên nhân bằng lời thường | Cách sửa |
|---|---|---|---|
catch (RuntimeException e) đứng trước catch (ArithmeticException e) | exception ArithmeticException has already been caught | Cái rộng đã hứng hết, cái hẹp không bao giờ tới lượt | Đặt catch hẹp trước, rộng sau |
try { ... } mà không có catch hay finally | 'try' without 'catch', 'finally' or resource declarations | try một mình không có ý nghĩa, phải kèm người xử lý hoặc dọn dẹp | Thêm catch hoặc finally |
catch (java.io.IOException e) khi try không làm gì liên quan I/O | exception IOException is never thrown in body of corresponding try statement | Java biết loại checked đó không thể xuất hiện nên cấm hứng nó | Bỏ catch, hoặc try phải có dòng có thể ném loại đó |
catch (NumberFormatException e) { } rỗng | Không có thông báo nào: swallow abc 0 và swallow 0 0 ra cùng kết quả | Lỗi biến mất, không ai biết đã có chuyện | In thông báo hoặc trả giá trị phân biệt được, như -1 |
Dòng cuối là hiểu lầm nguy hiểm nhất vì chương trình không báo gì, nên mình gom nó vào một hình.
Cách đọc hình: hàng trên màu đỏ nhạt là mô hình sai: nghĩ rằng catch rỗng làm chương trình an toàn nên dữ liệu hỏng đã được xử lý. Hàng dưới màu lục là mô hình đúng: lỗi chỉ biến mất, không ai được báo, và abc hay 0 đều ra 0. Mỗi ô đều có nhãn chữ, màu chỉ là tín hiệu phụ.
Bạn vừa làm được: đọc ba thông báo lỗi biên dịch của try và catch, và nhận ra vì sao catch rỗng nguy hiểm.
Tự tay làm (Bài tập)
Làm lần lượt từng mức. Mỗi bài có đề, dữ liệu vào, kết quả mong đợi, tiêu chí nghiệm thu và gợi ý. Lời giải ở cuối bài, hãy thử trước rồi mới so.
Mức Cơ bản
Bài 1. Điền bảng trace. Dữ liệu vào: chương trình SafeReader ở trên nhưng mảng đổi thành {"cake,3", "soda,0", "water", "juice,two"}. Điền bảng bốn dòng gồm các cột: đơn, lỗi (nếu có), catch chạy, các dòng in ra theo thứ tự. Kết quả mong đợi: một bảng bốn dòng, mỗi đơn in hai dòng. Tiêu chí nghiệm thu: bạn điền xong trước khi chạy rồi chạy thật để so, và end chắc chắn xuất hiện. Gợi ý: 300 / 3 bằng 100.
Bài 2. Sửa thứ tự catch. Dữ liệu vào là đoạn sau, bị lỗi biên dịch:
try {
System.out.println(1 / 0);
} catch (RuntimeException e) {
System.out.println("general");
} catch (ArithmeticException e) {
System.out.println("specific");
}
Sửa để biên dịch được và in specific. Kết quả mong đợi: specific. Tiêu chí nghiệm thu: bạn giải thích được vì sao hai catch phải đổi chỗ. Gợi ý: ArithmeticException là con của RuntimeException.
Bài 3. Dự đoán có finally. Dữ liệu vào: try in A, gọi Integer.parseInt("x"), in B; catch (ArithmeticException e) in C; finally in D; sau cả câu lệnh in E (dùng print, không xuống dòng). Dự đoán những chữ được in ra màn hình thường, và chuyện gì xảy ra với E. Kết quả mong đợi: bạn viết ra chuỗi chữ. Tiêu chí nghiệm thu: so với chạy thật. Gợi ý: catch này có khớp lỗi của parseInt không?
Mức Vận dụng
Bài 4. Thống kê đơn của quán. Sửa chương trình đọc đơn để đếm số đơn tốt, số đơn hỏng, và số đơn đã kiểm tra, gom cả ba loại lỗi vào một catch bằng |, và in thêm một dòng tổng kết. Dữ liệu vào: {"latte,2", "mocha,x", "tea", "cake,0"}. Kết quả mong đợi:
latte,2 total 150
mocha,x rejected: java.lang.NumberFormatException: For input string: "x"
tea rejected: java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 1
cake,0 rejected: java.lang.ArithmeticException: / by zero
ok 1, bad 3, checked 4
Tiêu chí nghiệm thu: năm dòng khớp từng ký tự, và số đã kiểm tra tăng ở finally. Gợi ý: catch (A | B | C e) rồi in e ra.
Mức Thử thách (tuỳ chọn)
Bài 5. Báo lỗi thay vì nuốt. Viết report(String text) trả về số nếu text là số, còn nếu không thì in bad number: kèm text rồi trả về -1. Dữ liệu vào: report("abc") và report("0"). Kết quả mong đợi: abc cho -1 và có dòng thông báo, 0 cho 0. Tiêu chí nghiệm thu: hai kết quả phân biệt được, và bạn nêu được một hạn chế của việc dùng -1 làm dấu hiệu lỗi. Gợi ý: nếu dữ liệu hợp lệ có thể bằng -1 thì sao?
Tự kiểm tra (Retrieval Practice)
Trả lời bằng trí nhớ, không xem lại bài. Mỗi câu ghi rõ nó kiểm tra mục tiêu nào trong bốn mục tiêu ở đầu bài.
Câu 1 (nhớ lại, mục tiêu 2). Nêu vai trò của ba khối try, catch, finally mỗi khối một câu.
Câu 2 (dự đoán kết quả code, mục tiêu 2). Đoạn sau in ra gì?
try {
System.out.print("A");
int x = 1 / 0;
System.out.print("B");
} catch (ArithmeticException e) {
System.out.print("C");
} finally {
System.out.print("D");
}
System.out.println("E");
Câu 3 (dự đoán trạng thái sau nhiều bước, mục tiêu 2). Giống bài 3 nhưng với parseInt("x") và catch (ArithmeticException e): chữ nào ra màn hình thường, E có in không, và chương trình kết thúc thế nào?
Câu 4 (tìm lỗi, mục tiêu 4). Một try có catch (Exception e) đứng trước catch (NumberFormatException e). Chỉ ra lỗi và sửa.
Câu 5 (giải thích cho bạn chưa học, mục tiêu 3). Bạn của bạn hỏi: "Sao phải có finally, viết dòng dọn dẹp ở cuối cũng được mà?" Hãy giải thích bằng việc dọn quầy cuối ca, và nói khi nào dòng ở cuối sẽ không chạy.
Điều cần nhớ
trychứa dòng có thể lỗi,catchhứng đúng loại lỗi, và lỗi cắt ngangtrynên các dòng sau chỗ lỗi bị bỏ qua (mục tiêu 1).- Đường đi có ba kết cục: không lỗi, lỗi có
catchkhớp, lỗi không khớp, vàfinallychạy trong cả ba (mục tiêu 2). catchhẹp đứng trướccatchrộng, vàfinallychạy trước khireturnhoàn tất (mục tiêu 3).- Ba lỗi hay gặp:
catchrỗng, thứ tựcatchsai,trytrơ trọi (mục tiêu 4).
Cheat sheet một trang.
| Việc cần làm | Cú pháp | Ghi chú |
|---|---|---|
| Chặn một loại lỗi | try { ... } catch (Type e) { ... } | Lỗi cắt ngang try, dòng sau chỗ lỗi bị bỏ |
| Nhiều loại, xử lý riêng | nhiều khối catch | Thử từ trên xuống, hẹp trước rộng sau |
| Nhiều loại, xử lý chung | một catch ghi các loại cách nhau bằng dấu gạch đứng | Hai loại không được là cha con của nhau |
| Dọn dẹp luôn chạy | finally { ... } | Chạy cả khi có return hoặc lỗi không khớp |
| Báo lỗi, không nuốt | in e hoặc trả giá trị phân biệt | catch rỗng làm mất dấu vết |
Cách đọc hình: mỗi hàng là một khối bên trái và vai trò của nó bên phải. try dừng ở lỗi đầu tiên, catch khớp đầu tiên chạy và phải nói điều đã xảy ra, finally chạy trên mọi con đường.
Câu hỏi để dành cho ngày mai: hàm report trả -1 để báo lỗi, nhưng nếu dữ liệu hợp lệ cũng có thể bằng -1 thì sao? Có cách nào để chính hàm báo lỗi lên cho nơi gọi tự quyết định không?
Đào sâu
Phần này dành cho bạn tò mò. Bỏ qua cũng không thiếu gì ở trên.
Cơ chế bên trong. Với try (Machine a = new Machine("A"); Machine b = new Machine("B")) (gọi là try-with-resources, với Machine là lớp có close() và khai báo implements AutoCloseable), kết quả là open A, open B, use, close B, close A [Đã chạy, JDK 17.0.15]: tài nguyên mở sau được đóng trước, và đóng tự động mà không cần viết finally.
So với lựa chọn khác. Mình thử một phương thức có throw trong try và return 7 trong finally. Nó trả về 7 và ngoại lệ biến mất, không ai thấy [Đã chạy, JDK 17.0.15]. Vì vậy đừng đặt return hay throw trong finally: nó che mất lỗi đang bay.
Hiểu lầm nâng cao. Nhiều người nghĩ finally chạy mọi lúc. Gần đúng nhưng không tuyệt đối: nếu chương trình bị dừng cưỡng bức trước đó (ví dụ gọi lệnh thoát) hoặc máy ảo bị tắt, finally không có cơ hội chạy. Bài này không thử lại điều đó, nên đây chỉ là lời nhắc.
Giới hạn của bài. Bài này chưa nói về throw và throws để chủ động ném và khai báo lỗi, ngoại lệ tự tạo, chuỗi nguyên nhân (cause), và tài nguyên kèm ngoại lệ bị chồng (suppressed). Ba việc đầu nằm ở bài sau. Mọi kết quả là của JDK 17.0.15.
Đọc thêm (tài liệu chính thống, Java SE 17):
- The Java Tutorials: The try Block
- The Java Tutorials: The catch Blocks
- The Java Tutorials: The finally Block
- The Java Tutorials: The try-with-resources Statement
Bài tiếp theo
Bài sau chuyển từ bắt lỗi sang chủ động ném lỗi: throw, throws, và tự viết lớp ngoại lệ riêng của quán.
Mini-dự án nối tiếp: giữ lại chương trình đọc đơn có try, catch, finally. Ở bài sau, hàm process sẽ tự ném ra một ngoại lệ riêng mang tên InvalidOrderException thay vì để lỗi gốc rò ra ngoài.
Lời giải và đáp án
Hãy thử xong rồi mới đọc phần này.
Lời giải Làm cùng mình. Các dòng thay cho // TODO [Đã chạy, JDK 17.0.15, cho ra sáu dòng ở bước 3]:
System.out.println(drink.length());
System.out.println("no drink");
} finally {
// Đóng quầy dù có lỗi hay không
System.out.println("close");
}
| Dòng code | Máy làm gì | Lý do chọn cách này |
|---|---|---|
drink.length() | Với "latte" cho 5, với null ném NullPointerException | Dòng này là nguồn của lỗi nên đặt trong try |
System.out.println("no drink"); | In khi bắt đúng NullPointerException | Nói rõ chuyện gì xảy ra thay vì im lặng |
finally { ... "close" ... } | In close ở cả hai lần gọi | Đóng quầy là việc dọn dẹp không phụ thuộc lỗi |
Bước 4: đặt catch (RuntimeException e) lên trước thì có lỗi dạng exception NullPointerException has already been caught, cùng dạng với dòng đầu của bảng lỗi [Suy luận, dựa trên thông báo đã chạy thật ở bảng lỗi].
Lời giải Bài 1. [Suy luận, khớp với kết quả chạy thật]
| Đơn | Lỗi | catch chạy | In ra theo thứ tự |
|---|---|---|---|
cake,3 | không | không | cake,3 total 100, checked cake,3 |
soda,0 | ArithmeticException | ArithmeticException | soda,0 zero quantity, checked soda,0 |
water | ArrayIndexOutOfBoundsException | ArrayIndexOutOfBoundsException | water missing quantity, checked water |
juice,two | NumberFormatException | NumberFormatException | juice,two bad number, checked juice,two |
Lời giải Bài 2. [Suy luận] Đổi chỗ hai catch, ArithmeticException đứng trước:
} catch (ArithmeticException e) {
System.out.println("specific");
} catch (RuntimeException e) {
System.out.println("general");
}
In ra specific. Hai catch phải đổi chỗ vì ArithmeticException là con của RuntimeException và catch đầu khớp cả con.
Lời giải Bài 3. [Đã chạy] Màn hình thường in AD ngay, rồi dấu vết NumberFormatException. Chữ E không in. Lý do: catch (ArithmeticException e) không khớp NumberFormatException nên C không in, finally vẫn in D, rồi ngoại lệ trèo lên làm chương trình dừng nên E không bao giờ chạy.
Lời giải Bài 4. [Đã chạy, năm dòng khớp kết quả mong đợi] Phần then chốt của vòng lặp:
try {
String[] parts = line.split(",");
int qty = Integer.parseInt(parts[1]);
System.out.println(line + " total " + 300 / qty);
ok++;
} catch (NumberFormatException | ArrayIndexOutOfBoundsException | ArithmeticException e) {
// Multi-catch: in e sẽ ra tên loại lỗi kèm thông báo
System.out.println(line + " rejected: " + e);
bad++;
} finally {
checked++;
}
| Dòng code | Máy làm gì | Lý do chọn cách này |
|---|---|---|
ok++; cuối try | Đếm đơn tốt | Chỉ chạy khi cả ba dòng trước không lỗi |
catch ghi ba loại lỗi nối bằng dấu gạch đứng | Một khối cho ba loại lỗi | Cách xử lý giống nhau nên không cần ba khối |
bad++; trong catch | Đếm đơn hỏng | Chỉ chạy khi có lỗi |
checked++; trong finally | Đếm mọi đơn đã qua | Chạy ở cả đơn tốt và đơn hỏng |
Gợi ý lời giải Bài 5. [Đã chạy] Khuôn của report:
static int report(String text) {
try {
return Integer.parseInt(text);
} catch (NumberFormatException e) {
System.out.println("bad number: " + text);
return -1;
}
}
report("abc") in bad number: abc và trả -1, report("0") trả 0. Hạn chế: nếu dữ liệu hợp lệ có thể bằng -1 thì không phân biệt được với lỗi. Bài sau cho bạn cách ném lỗi lên thay vì trả số đặc biệt.
Đáp án Tự kiểm tra.
trychứa các dòng có thể lỗi,catchxử lý một loại lỗi cụ thể,finallylà việc dọn dẹp luôn chạy. Kiểm tra mục tiêu 2.- In ra
ACDE[Đã chạy]:Ain, phép chia ném lỗi nênBbị bỏ qua,catchinC,finallyinD, rồiEở ngoài. Kiểm tra mục tiêu 2. - Màn hình thường in
AD,Ekhông in, chương trình kết thúc với dấu vếtNumberFormatExceptionvì khôngcatchnào khớp [Đã chạy]. Kiểm tra mục tiêu 2. catch (Exception e)đứng trước đã khớp mọiNumberFormatException, nên cái sau không bao giờ chạy và Java báohas already been caught. Sửa: đặtcatch (NumberFormatException e)lên trước. Kiểm tra mục tiêu 4.- Dòng dọn dẹp viết ở cuối chỉ chạy khi mọi thứ trước đó không ném lỗi hay không
return.finallychạy cả khi có lỗi không ai bắt hoặc córeturn, như việc dọn quầy vẫn phải làm dù ca có đổ vỡ. Dòng ở cuối sẽ không chạy khitrygặp lỗi không khớpcatchhoặc gặpreturn. Kiểm tra mục tiêu 3.