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 try và catch bắ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, catch và finally trong ba trường hợp: không lỗi, lỗi có catch khớp, và lỗi không catch nào khớp.
  • Giải thích được vì sao catch hẹp phải đứng trước catch rộng, và vì sao finally chạy cả khi try đã gặp return.
  • Phát hiện và sửa được ba lỗi hay gặp: catch rỗng nuốt lỗi, catch rộng đứng trước, và try thiếu catch hoặc finally.

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 split và parseInt bắt đầu chạy, dòng in total thì không.
  • C. Chỉ split, vì parseInt bị 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à ArithmeticException luôn rơi vào catch đầu.
  • B. Biên dịch được, và Java chọn catch hẹp nhất bất kể thứ tự.
  • C. Biên dịch được nhưng catch thứ hai không bao giờ chạy.
  • D. Lỗi biên dịch, vì catch thứ 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]:

java
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ả:

text
finally runs first
1
finally still runs
outer caught java.lang.NumberFormatException: For input string: "x"
Dòng codeMáy làm gìVì sao viết như vậy
return 1; trong tryTính giá trị 1 nhưng chưa rời phương thứcPhả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àyparseInt ném NumberFormatException nên không khớp, thân không chạy
finally { ... } trong noMatchChạy dù không catch nào khớpViệ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) ở mainBắt ngoại lệ đang trèo lênCó 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.

java
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]:

text
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

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]:

text
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 codeMáy làm gìVì sao viết như vậy
for (String line : lines) { process(line); }Gọi process cho từng đơnVò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 / qtyChia 300 cho số lượngSố 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ẩyBa 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 đơnViế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ặpChạ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):

ĐơnDòng của try chạyLỗicatch chạyfinallyIn ra
latte,2cả ba dòngkhôngkhôngcólatte,2 total 150, checked latte,2
mocha,xsplit, parseIntNumberFormatExceptionNumberFormatExceptioncómocha,x bad number, checked mocha,x
teasplit, parseIntArrayIndexOutOfBoundsExceptionArrayIndexOutOfBoundsExceptioncótea missing quantity, checked tea
cake,0cả ba dòngArithmeticExceptionArithmeticExceptioncó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 finally in trước, rồi phương thức trả về 1.
  • B. Phương thức trả về 1 trước, finally in sau khi nơi gọi đã nhận 1.
  • C. finally bị bỏ qua vì return kết thúc phương thức ngay.
  • D. Lỗi biên dịch, vì try có return thì 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.parseInt luôn trả về 0 khi gặp chữ.
  • B. catch bắ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 0 nằm ngoài try.

Đá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,5 không phải một số.
  • B. In latte,2,5 total 150, rồi checked 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.

java
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ỗiThông báo lỗiNguyên nhân bằng lời thườngCách sửa
catch (RuntimeException e) đứng trước catch (ArithmeticException e)exception ArithmeticException has already been caughtCá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 declarationstry một mình không có ý nghĩa, phải kèm người xử lý hoặc dọn dẹpThêm catch hoặc finally
catch (java.io.IOException e) khi try không làm gì liên quan I/Oexception IOException is never thrown in body of corresponding try statementJava 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ỗngKhô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ệnIn 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.

Hình: mô hình sai và mô hình đúng về khối catch rỗng

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:

java
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:

text
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ì?

java
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ớ

  • try chứa dòng có thể lỗi, catch hứng đúng loại lỗi, và lỗi cắt ngang try nê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ó catch khớp, lỗi không khớp, và finally chạy trong cả ba (mục tiêu 2).
  • catch hẹp đứng trước catch rộng, và finally chạy trước khi return hoàn tất (mục tiêu 3).
  • Ba lỗi hay gặp: catch rỗng, thứ tự catch sai, try trơ trọi (mục tiêu 4).

Cheat sheet một trang.

Việc cần làmCú phápGhi chú
Chặn một loại lỗitry { ... } catch (Type e) { ... }Lỗi cắt ngang try, dòng sau chỗ lỗi bị bỏ
Nhiều loại, xử lý riêngnhiều khối catchThử từ trên xuống, hẹp trước rộng sau
Nhiều loại, xử lý chungmột catch ghi các loại cách nhau bằng dấu gạch đứngHai loại không được là cha con của nhau
Dọn dẹp luôn chạyfinally { ... }Chạy cả khi có return hoặc lỗi không khớp
Báo lỗi, không nuốtin e hoặc trả giá trị phân biệtcatch rỗng làm mất dấu vết
Hình: ba khối try, catch, finally và việc mỗi khối làm

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):

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]:

java
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 codeMáy làm gìLý do chọn cách này
drink.length()Với "latte" cho 5, với null ném NullPointerExceptionDò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 NullPointerExceptionNó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]

ĐơnLỗicatch chạyIn ra theo thứ tự
cake,3khôngkhôngcake,3 total 100, checked cake,3
soda,0ArithmeticExceptionArithmeticExceptionsoda,0 zero quantity, checked soda,0
waterArrayIndexOutOfBoundsExceptionArrayIndexOutOfBoundsExceptionwater missing quantity, checked water
juice,twoNumberFormatExceptionNumberFormatExceptionjuice,two bad number, checked juice,two

Lời giải Bài 2. [Suy luận] Đổi chỗ hai catch, ArithmeticException đứng trước:

java
} 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:

java
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 codeMáy làm gìLý do chọn cách này
ok++; cuối tryĐếm đơn tốtChỉ 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 đứngMột khối cho ba loại lỗiCách xử lý giống nhau nên không cần ba khối
bad++; trong catchĐếm đơn hỏngChỉ chạy khi có lỗi
checked++; trong finallyĐếm mọi đơn đã quaChạ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:

java
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.

  1. try chứa các dòng có thể lỗi, catch xử lý một loại lỗi cụ thể, finally là việc dọn dẹp luôn chạy. Kiểm tra mục tiêu 2.
  2. In ra ACDE [Đã chạy]: A in, phép chia ném lỗi nên B bị bỏ qua, catch in C, finally in D, rồi E ở ngoài. Kiểm tra mục tiêu 2.
  3. Màn hình thường in AD, E không in, chương trình kết thúc với dấu vết NumberFormatException vì không catch nào khớp [Đã chạy]. Kiểm tra mục tiêu 2.
  4. catch (Exception e) đứng trước đã khớp mọi NumberFormatException, nên cái sau không bao giờ chạy và Java báo has already been caught. Sửa: đặt catch (NumberFormatException e) lên trước. Kiểm tra mục tiêu 4.
  5. 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. finally chạ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 khi try gặp lỗi không khớp catch hoặc gặp return. Kiểm tra mục tiêu 3.