Một exception (ngoại lệ) là một lỗi có thể xảy ra trong quá trình thực thi chương trình và làm gián đoạn luồng xử lý thông thường. Java cung cấp một cơ chế mạnh mẽ và có tính hướng đối tượng để xử lý ngoại lệ, được gọi là Java Exception Handling.
Exception trong Java có thể phát sinh từ nhiều tình huống khác nhau, ví dụ như người dùng nhập sai dữ liệu, lỗi phần cứng, mất kết nối mạng, hoặc database server bị lỗi. Phần code quy định cách xử lý các tình huống exception cụ thể này được gọi là exception handling.
Ném và bắt ngoại Lệ (Throwing and Catching Exceptions)
Khi một lỗi xảy ra trong quá trình thực thi một câu lệnh, Java sẽ tạo ra một đối tượng exception. Nó chứa rất nhiều thông tin dùng để debug lỗi như thứ bậc của method, số dòng nơi exception xảy ra và loại exception.
Nếu một exception xảy ra trong một method, quá trình tạo đối tượng exception và giao nó cho môi trường runtime được gọi là “ném ngoại lệ” (throwing the exception). Lúc này, lồng thực thi của chương trình sẽ bị tạm dừng và Java Runtime Environment (JRE) sẽ cố gắng tìm một trình xử lý ngoại lệ (exception handler) phù hợp
Exception Handler là một khối code có khả năng xử lý đối tượng exception.
- Logic tìm kiếm exception handler bắt đầu từ method nơi lỗi xảy ra.
- Nếu không tìm thấy handler phù hợp, nó sẽ di chuyển lên method đã gọi nó (caller method).
- Quá trình này cứ tiếp diễn như vậy.
Do đó, nếu call stack (một cấu trúc dữ liệu theo dõi thứ tự các hàm được gọi trong chương trình) của các method là A() -> B() -> C()
và một exception phát sinh trong method C()
, thì việc tìm kiếm handler phù hợp sẽ di chuyển từ C() -> B() -> A()
.
Khi tìm thấy một exception handler phù hợp, đối tượng exception sẽ được chuyển đến handler đó để xử lý. Ta nói rằng handler này đã “bắt ngoại lệ” (catching the exception). Nếu không tìm thấy handler nào phù hợp, chương trình sẽ kết thúc và in thông tin về exception ra console.
Framework xử lý exception của Java chỉ được dùng để xử lý các lỗi runtime. Các lỗi lúc biên dịch (compile-time) phải được lập trình viên sửa trực tiếp trong mã nguồn, nếu không chương trình sẽ không thể thực thi.
Các từ khóa liên quan đến việc xử lý Exception trong Java
Java cung cấp các từ khóa cụ thể cho mục đích xử lý exception.
- throw: Ta đã biết rằng nếu có lỗi xảy ra, một đối tượng exception sẽ được tạo ra và Java runtime bắt đầu xử lý chúng. Đôi khi, ta có thể muốn tự tạo ra exception một cách tường minh có chủ đích trong code của mình. Ví dụ, trong một chương trình xác thực người dùng, ta nên throw exception cho client nếu mật khẩu không hợp lệ. Từ khóa
throw
được sử dụng để đưa exception cho runtime xử lý. - throws: Khi ta throw một exception trong một method mà không xử lý nó, ta phải sử dụng từ khóa
throws
trong chữ ký của method để cho chương trình gọi nó biết các exception có thể được tạo ra từ method này. Method gọi (caller) có thể xử lý các exception này hoặc chuyển tiếp chúng lên caller của nó bằng từ khóathrows
. Ta có thể khai báo nhiều exception trong mệnh đềthrows
và nó cũng có thể được sử dụng với methodmain
. - try-catch: Ta sử dụng khối
try-catch
để nhận lấy và xử lý exception trong code.try
là điểm bắt đầu của khối vàcatch
nằm ở cuối khốitry
để xử lý các exception. Ta có thể có nhiều khốicatch
với một khốitry
. Khốitry-catch
cũng có thể được lồng vào nhau. Khốicatch
yêu cầu một tham số phải có kiểu là Exception. - finally: Khối
finally
không bắt buộc phải có và chỉ có thể được sử dụng với khốitry-catch
. Vì exception làm tạm dừng quá trình thực thi, có thể có một số tài nguyên đang mở mà không được đóng lại. Trường hợp này ta có thể sử dụng khốifinally
để giải quyết các tafin nguyên đó. Nó luôn được thực thi, bất kể exception có xảy ra hay không.
Ví dụ về xử lý Exception
package com.journaldev.exceptions;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionHandling {
public static void main(String[] args) throws FileNotFoundException, IOException {
try {
testException(-5);
testException(-10);
} catch(FileNotFoundException e) {
e.printStackTrace();
} catch(IOException e) {
e.printStackTrace();
} finally {
System.out.println("Releasing resources");
}
testException(15);
}
public static void testException(int i) throws FileNotFoundException, IOException {
if (i < 0) {
FileNotFoundException myException = new FileNotFoundException("Negative Integer " + i);
throw myException;
} else if (i > 10) {
throw new IOException("Only supported for index 0 to 10");
}
}
}
- Method
processFile()
tạo exception bằng từ khóathrow
. Chữ ký của method sử dụng từ khóathrows
để cho bên gọi biết các loại exception mà nó có thể ném ra. - Method
main()
xử lý các exception bằng khốitry-catch
. Khi không xử lý chúng, nó sẽ lan truyền các lỗi đến runtime bằng mệnh đềthrows
. - Câu lệnh
testException(-10)
không bao giờ được thực thi do có exception trước đó, và sau đó khốifinally
sẽ được thực thi.
printStackTrace()
là một trong những method hữu ích nhất trong class Exception
cho mục đích debug.
Đoạn code trên sẽ cho ra output sau:
java.io.FileNotFoundException: Negative Integer -5
at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24)
at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10)
Releasing resources
Exception in thread "main" java.io.IOException: Only supported for index 0 to 10
at com.journaldev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27)
at com.journaldev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)
Một số điểm quan trọng cần lưu ý:
- Ta không thể có mệnh đề
catch
hoặcfinally
mà không có câu lệnhtry
. - Một câu lệnh
try
phải có hoặc khốicatch
hoặc khốifinally
, và cũng có thể có cả hai. - Ta không thể viết bất kỳ code nào giữa các khối
try-catch-finally
. - Ta có thể có nhiều khối
catch
với một câu lệnhtry
. - Các khối
try-catch
có thể được lồng vào nhau tương tự như câu lệnhif-else
. - Ta chỉ có thể có một khối
finally
với một câu lệnhtry
.
Hệ thống phân cấp Exception của Java
Như đã đề cập trước đó, khi một lỗi xuất hiện, một đối tượng exception sẽ được tạo. Các exception trong Java được tổ chức theo hệ thống phân cấp và sử dụng tính kế thừa để phân loại chúng.
Throwable
là class cha của hệ thống phân cấp các exception trong Java. Nó có hai đối tượng con: Error và Exception. Các Exception lại được chia thành Checked Exception và Runtime Exception.
- Errors: Đây là các trường hợp đặc biệt nằm ngoài phạm vi của ứng dụng mà chúng ta không thể lường trước và phục hồi từ chúng. Các ví dụ thông dụng là lỗi phần cứng, máy ảo Java (JVM) bị lỗi và dừng, hoặc do hết bộ nhớ. Đó là lý do tại sao chúng ta có một hệ thống phân cấp Error riêng và không nên cố gắng xử lý những tình huống này. Một số Error phổ biến là OutOfMemoryError và StackOverflowError.
- Checked Exceptions: Đây là các kịch bản lỗi mà ta có thể lường trước trong một chương trình và cố gắng phục hồi từ đó. Ví dụ như
FileNotFoundException
. Ta nên nhận lấy exception này, cung cấp một thông báo hữu ích cho người dùng và ghi log lại để tiện cho việc debug. Exception là class cha của tất cả các Checked Exception. Nếu ta tạo ra một Checked Exception, ta phải xử lý nó trong cùng một method hoặc phải chuyển tiếp nó cho method gọi bằng từ khóathrows
. - Runtime Exception: Đây là những lỗi gây ra do lập trình sai. Ví dụ, khi ta cố gắng lấy một phần tử từ một mảng. Ta nên kiểm tra độ dài của mảng trước khi cố gắng lấy phần tử, nếu không nó có thể throw
ArrayIndexOutOfBoundsException
tại thời điểm runtime. RuntimeException là class cha của tất cả các Runtime Exception. Nếu ta throw bất kỳRuntimeException
nào trong một method, không bắt buộc phải khai báo chúng trong mệnh đềthrows
của chữ ký method. Các Runtime Exception có thể được tránh bằng cách lập trình cẩn thận hơn.
Một số method hữu ích của các class Exception
Throwable
và tất cả các class con của nó không cung cấp bất kỳ method đặc thù nào. Tất cả các method đều được định nghĩa trong class cơ sở Throwable
. Các class Exception
được tạo ra để xác định các loại kịch bản lỗi khác nhau để ta có thể dễ dàng xác định nguyên nhân gốc rễ và xử lý Exception
tùy theo loại của nó. Class Throwable
triển khai interface Serializable
để đảm bảo khả năng tương thích với nhau.
Một số method hữu ích của class Throwable
là:
public String getMessage()
: Method này trả vềString
thông báo củaThrowable
và thông báo này có thể được cung cấp khi tạo exception thông qua constructor của nó.public String getLocalizedMessage()
: Method này được cung cấp để các class con có thể ghi đè nó nhằm cung cấp một thông báo theo ngôn ngữ cụ thể cho chương trình gọi. Cách triển khai của method này trong classThrowable
chỉ đơn giản là sử dụng methodgetMessage()
để trả về thông báo exception.public synchronized Throwable getCause()
: Method này trả về nguyên nhân của exception hoặcnull
nếu không rõ nguyên nhân.public String toString()
: Method này trả về thông tin vềThrowable
dưới định dạngString
, chứa tên của classThrowable
và thông báo đã được địa phương hóa.public void printStackTrace()
: Method này in thông tin stack trace (danh sách các hàm đã gọi đến khi xảy ra lỗi) ra standard error stream (luồng dữ liệu riêng biệt để gửi thông báo lỗi). Method này được nạp chồng và ta có thể truyềnPrintStream
hoặcPrintWriter
làm đối số để ghi thông tin stack trace ra file hoặc stream.
Các cải tiến về quản lý tài nguyên tự động và khối Catch trong Java 7
Khi xử lý nhiều exception trong một khối try
, bạn sẽ nhận thấy rằng khối catch
hay chứa các đoạn code lặp đi lặp lại để ghi log lỗi. Trong Java 7, một trong những tính năng mới là ta có thể xử lý nhiều exception trong một khối catch
duy nhất.
Dưới đây là một ví dụ về khối catch
với tính năng này:
catch (IOException | SQLException ex) {
logger.error(ex);
throw new MyException(ex.getMessage());
}
Sẽ có một số ràng buộc, ví dụ như việc đối tượng exception là final và ta không thể sửa đổi nó bên trong khối catch
.
Trong hầu hết trường hợp, ta sử dụng khối finally
chỉ để đóng các tài nguyên. Đôi khi ta quên đóng chúng và gặp các exception runtime khi tài nguyên bị cạn kiệt. Những exception này rất khó để debug, và ta có thể phải xem xét mọi nơi đã sử dụng tài nguyên đó để đảm bảo rằng ta đã đóng nó.
Java 7 thêm một tính năng mới là try-with-resources
. Ta có thể dùng nó để tạo một tài nguyên ngay trong câu lệnh try
và sử dụng nó bên trong khối try-catch
. Khi chương trình thoát ra khỏi khối try
, môi trường runtime sẽ tự động đóng các tài nguyên này.
Dưới đây là một ví dụ về khối try-catch
với cải tiến này:
try (MyResource mr = new MyResource()) {
System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
e.printStackTrace();
}
Ví dụ về một Class Exception tùy chỉnh
Java cung cấp rất nhiều class exception để chúng ta sử dụng, nhưng đôi khi ta có thể cần tạo các class tùy chỉnh của riêng mình. Ví dụ, ta có thể dùng nó để thông báo cho caller về một loại exception cụ thể với thông báo thích hợp. Ta cũng có thể có các trường tùy chỉnh để theo dõi mã lỗi.
Giả sử ta viết một method chỉ để xử lý các file văn bản. Ta có thể cung cấp cho caller mã lỗi phù hợp khi một loại file khác nào đó cung cấp ở đầu vào.
Đầu tiên, tạo file MyException.java
:
package com.journaldev.exceptions;
public class MyException extends Exception {
private static final long serialVersionUID = 4664456874499611218L;
private String errorCode = "Unknown_Exception";
public MyException(String message, String errorCode) {
super(message);
this.errorCode=errorCode;
}
public String getErrorCode() {
return this.errorCode;
}
}
Sau đó, tạo CustomExceptionExample.java
:
package com.journaldev.exceptions;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class CustomExceptionExample {
public static void main(String[] args) throws MyException {
try {
processFile("file.txt");
} catch (MyException e) {
processErrorCodes(e);
}
}
private static void processErrorCodes(MyException e) throws MyException {
switch (e.getErrorCode()) {
case "BAD_FILE_TYPE":
System.out.println("Bad File Type, notify user");
throw e;
case "FILE_NOT_FOUND_EXCEPTION":
System.out.println("File Not Found, notify user");
throw e;
case "FILE_CLOSE_EXCEPTION":
System.out.println("File Close failed, just log it.");
break;
default:
System.out.println("Unknown exception occured, lets log it for further debugging." + e.getMessage());
e.printStackTrace();
}
}
private static void processFile(String file) throws MyException {
InputStream fis = null;
try {
fis = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new MyException(e.getMessage(), "FILE_NOT_FOUND_EXCEPTION");
} finally {
try {
if (fis != null) fis.close();
} catch (IOException e) {
throw new MyException(e.getMessage(), "FILE_CLOSE_EXCEPTION");
}
}
}
}
Ta có thể có một method riêng để xử lý các loại mã lỗi khác nhau mà ta nhận được từ các method khác nhau. Một số trong số chúng sẽ được consume (được xử lý và không còn tồn tại) vì ta có thể không muốn thông báo cho người dùng về điều đó. Hoặc một số khác ta sẽ throw lại để thông báo cho người dùng về vấn đề.
Ở đây ta đang kế thừa từ Exception
để bất cứ khi nào exception này được tạo ra, nó phải được xử lý trong method hoặc được trả về cho chương trình gọi. Nếu kế thừa từ RuntimeException
, ta không cần phải khai báo nó trong mệnh đề throws
.
Đây là một quyết định có chủ đích khi thiết kế chương trình. Sử dụng Checked Exception có lợi thế là giúp các lập trình viên hiểu được những exception nào họ có thể gặp phải và có hành động phù hợp để xử lý chúng.
Những lời khuyên khi xử lý Exception trong Java
- Sử dụng Exception cụ thể: Các class cơ sở của hệ thống phân cấp Exception không cung cấp thông tin hữu ích nào. Đó là lý do tại sao Java có rất nhiều class exception, chẳng hạn như
IOException
với các class con khác nhưFileNotFoundException
,SocketException
, v.v. Ta nên luônthrow
vàcatch
các class exception cụ thể để bên gọi có thể dễ dàng biết được nguyên nhân gốc rễ của exception và xử lý chúng một cách thích hợp. Điều này giúp việc debug dễ hơn và giúp các ứng dụng client xử lý exception phù hợp. - Throw sớm hoặc Fail-Fast: Ta nên cố gắng
throw
exception càng sớm càng tốt. Hãy xem xét methodprocessFile()
ở trên. Nếu ta truyền tham sốnull
vào, ta sẽ nhận được exception sau đây:
Exception in thread "main" java.lang.NullPointerException
at java.io.FileInputStream.<init>(FileInputStream.java:134)
at java.io.FileInputStream.<init>(FileInputStream.java:97)
at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42)
at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
Khi debug, ta sẽ phải xem xét kỹ stack trace để xác định vị trí thật sự đã gây ra exception. Ta có thể thay đổi logic để kiểm tra các exception này sớm hơn như sau:
private static void processFile(String file) throws MyException {
if (file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");
// ... further processing
}
Khi đó stack trace sẽ chỉ ra nơi exception xảy ra cùng với một thông báo rõ ràng:
com.journaldev.exceptions.MyException: File name can't be null
at com.journaldev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37)
at com.journaldev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
- Catch muộn: Vì Java bắt buộc phải xử lý checked exception hoặc khai báo nó trong chữ ký của method, đôi khi các lập trình viên có xu hướng catch exception và log lỗi. Thói quen này có hại vì chương trình gọi không nhận được bất kỳ thông báo nào về exception. Ta chỉ nên catch exception khi có thể xử lý chúng một cách thích hợp. Ví dụ, trong method trên, ta đang throw các exception trở lại cho method gọi để xử lý. Method đó cũng có thể được dùng bởi ứng dụng nào đó nữa mà nó lại muốn xử lý exception theo một cách khác. Khi triển khai bất kỳ tính năng nào, ta nên luôn throw exception trở lại cho bên gọi và để nó tự quyết định cách xử lý.
- Đóng tài nguyên: Vì exception làm tạm dừng quá trình xử lý của chương trình, ta nên đóng tất cả các tài nguyên trong khối
finally
hoặc sử dụng cải tiếntry-with-resources
của Java 7 để cho Java runtime tự động đóng chúng. - Ghi log Exception: Ta nên luôn ghi lại log của các thông báo exception và khi throw exception. Hãy cung cấp một thông báo rõ ràng để bên gọi có thể dễ dàng biết tại sao exception xảy ra. Ta nên luôn tránh khối
catch
trống chỉ consume exception mà không cung cấp bất kỳ chi tiết có ý nghĩa nào cho việc debug. - Dùng một khối catch cho nhiều exception: Hầu hết trường hợp chúng ta ghi log chi tiết exception và cung cấp thông báo cho người dùng. Trong trường hợp này, ta nên sử dụng tính năng của Java 7 để xử lý nhiều exception trong một khối
catch
duy nhất. Cách tiếp cận này sẽ giảm độ dài code và làm cho nó trông gọn gàng hơn. - Sử dụng Exception tùy chỉnh: Gần như luôn luôn phải xác định một chiến lược xử lý exception ngay ở giai đoạn thiết kế. Thay vì
throw
vàcatch
nhiều exception, ta có thể tạo một exception tùy chỉnh với mã lỗi riêng để chương trình gọi có thể xử lý các mã lỗi này. Ta cũng nên tạo một method tiện ích để xử lý các mã lỗi khác nhau và sử dụng chúng. - Quy ước đặt tên và đóng gói: Khi bạn tạo exception tùy chỉnh của mình, hãy đảm bảo tên của nó kết thúc bằng
Exception
để ngầm định đây là một class exception. Ngoài ra, hãy đóng gói chúng giống như cách được thực hiện trong Java Development Kit (JDK). Ví dụ,IOException
là exception cơ sở cho tất cả các tiến trình IO. - Sử dụng Exception một cách hợp lý: Exception khá tốn kém về mặt hiệu suất, và đôi khi ta không cần thiết phải throw exception .Thay vào đó, ta có thể trả về một biến boolean cho chương trình gọi để cho biết một hoạt động có thành công hay không. Điều này có ích khi hoạt động đó không mang tính bắt buộc và ta không muốn chương trình của mình bị kẹt chỉ vì nó chạy thất bại. Ví dụ, khi cập nhật giá cổ phiếu vào cơ sở dữ liệu từ một dịch vụ web của bên thứ ba, ta có thể muốn tránh throw exception nếu kết nối thất bại.
- Tài liệu hóa các Exception được throw: Sử dụng
@throws
của Javadoc để chỉ định rõ ràng các exception được throw bởi method. Thông tin này sẽ có ích khi bạn cung cấp một interface cho các ứng dụng khác để sử dụng.
Kết luận
Trong bài viết này, bạn đã tìm hiểu về xử lý exception trong Java. Bạn đã học về throw
và catch
, cũng như các khối try
(và try-with-resources
), catch
, và finally
.
Các kiến thức này sẽ giúp bạn viết các chương trình Java có khả năng xử lý lỗi phát sinh một cách thông minh và linh hoạt hơn, đảm bảo ứng dụng vận hành một cách ổn định và đáng tin cậy, ngay cả khi gặp phải lỗi không mong muốn.