Ngoại lệ là sự kiện không tránh khỏi, xuất hiện thường xuyên trong cuộc sống hàng ngày. Lấy ví dụ, khi nấu ăn, việc ta kiểm tra và điều chỉnh nhiệt độ, thời gian và nguyên liệu có thể được xem như một dạng luồng công việc (flow), tương tự như luồng công việc mà chúng ta định nghĩa trong lập trình.
Khi mọi thứ diễn ra như dự kiến, chúng ta có thể hoàn thành món ăn một cách trôi chảy, theo đúng trình tự và hướng dẫn của công thức. Tuy nhiên, có những lúc chúng ta phải đối mặt với những tình huống không lường trước được, ví dụ như thiếu nguyên liệu, gia vị được thêm quá mức, hoặc một số dụng cụ bị hỏng. Tùy vào từng tình huống cụ thể, chúng ta cần tìm ra các giải pháp riêng biệt để xử lý từng ngoại lệ.
Trong lập trình, cũng tương tự như vậy. Không phải lúc nào, mã lệnh của chúng ta cũng diễn ra suôn sẻ như kì vọng. Có vô số lý do có thể làm cho chương trình phải dừng lại và xử lý ngoại lệ. Việc xử lý ngoại lệ là cực kỳ quan trọng, giúp đảm bảo rằng mã lệnh có thể tiếp tục thực thi, hoặc ít ra, cung cấp thông báo lỗi rõ ràng khi đối mặt với ngoại lệ không mong muốn.
Tóm lại, việc hiểu và xử lý ngoại lệ không chỉ là một phần quan trọng của lập trình mà còn là một kỹ năng cần thiết trong cuộc sống, giúp chúng ta nhanh chóng phản ứng và thích ứng với mọi tình huống không lường trước được.
Chúng ta cùng tìm hiểu thêm về ngoại lệ trong lập trình nói chung và trong Java nói riêng, cũng như cách xử lý ngoại lệ trong bài viết này.
1. Exception là gì?
Trong lập trình nói chung, exception - hay ngoại lệ - là một sự kiện có thể xảy ra trong quá trình thực thi của chương trình, và nó có thể làm gián đoạn luồng xử lý thông thường của chương trình đó.
Khi những exception này xuất hiện trong lúc thực thi trường trình, chúng ta cần có một cơ chế để xử lý các ngoại lệ này, tránh việc các exception này làm gián đoạn, tổn hại hay chết chương trình. Quá trình trên gọi là exception handling (xử lý ngoại lệ)
Exception có thể phát sinh do nhiều lý do khác nhau, chẳng hạn như truy cập vùng nhớ không hợp lệ (NullPointerException) hay chia cho số không (ArithmeticException). Có rất nhiều những lý do khác nhau khiến mã nguồn của chúng ta gặp exception. Điều này khiến cho việc exception handling (xử lý ngoại lệ) trở nên vô cùng cần thiết.
2. Exception trong Java là gì?
Trong Java, Throwable là lớp cơ sở dành cho tất cả các đối tượng mà có thể được ném (throw) hoặc bắt (catch) bởi câu lệnh try-catch. Throwable có hai lớp con trực tiếp gồm Exception và Error.
Trong bài viết này, chúng ta sẽ làm rõ cả hai khái niệm trên.
2.1. Định nghĩa Exception và Error trong Java
Trong Java, Exception là một class rất đặc biệt được dùng để đại diện cho các ngoại lệ trong quá trình thực thi chương trình. Khi một class Exception được phát hiện, chúng ta cần phải xử lý ngoại lệ đó trong trương trình.
Bên cạnh đó, Java còn cùng cấp 1 lớp nữa tên là Error, đại diện cho một lỗi rất nghiêm trọng mà chương trình không nên bắt trong quá trình thực thi. Trong bài này cũng sẽ đề cập đến khái niệm error.
Cả error và exception đều kế thừa từ lớp throwable, cho phép chúng ta thực thi ném (throw) và bắt (catch) bởi câu lệnh try/catch.
Nói cách khác, khi chúng ta phát hiện được Exception, chương trình sẽ throw
exception
, và nhiệm vụ của chúng ta cần làm là catch exception
đó, và xử lý chúng. Cuối cùng, ngay cả khi có Exception xảy ra hay không, chúng ta có thể vẫn phải xử lý một số logic để hoàn tất việc xử lý chương trình.
Đối với Error, mặc dù có thể throw/catch, nhưng chương trình không nên xử lý với Error theo cách mà chương trình xử lý với Exception. Chúng ta sẽ nói rõ hơn ở phần sau.
2.2. Lý do Exception và Error xuất hiện trong chương trình
Exception và Error đều biểu diễn một vấn đề mà chương trình có thể gặp phải. Tuy nhiên, mục đích và bối cảnh sử dụng của cả 2 có chút khác biệt.
Exception là những sự cố mà lập trình viên có thể dự đoán và xử lý được. Chúng thường xuất hiện do dữ liệu đầu vào không hợp lệ, vấn đề khi kết nối tới một dịch vụ ngoài, v.v. Tất cả những trường hợp trên đều có thể dẫn đến việc exception được ném ra, làm gián đoạn luồng xử lý bình thường của chương trình.
Error biểu diễn những vấn đề nghiêm trọng mà chương trình không thể giải quyết hoặc xử lý được. Những lỗi này thường liên quan đến vấn đề về môi trường chạy của JVM, như hết bộ nhớ heap hoặc StackOverflow.
Một số error vẫn có thể xử lý được ở application layer, một số thì không thể xử lý được. Đa phần những lỗi này xảy ra rất khó dự đoán, khó nhận biết và gậy tổn hại đến hệ thống.
2.3. Tầm quan trọng của Exception Handling
Exception Handling chủ yếu giúp bảo vệ chương trình khỏi các sự cố và lỗi không dự đoán được, giữ cho chương trình tiếp tục hoạt động mà không bị gián đoạn, ngay cả khi gặp phải vấn đề. Việc này đặc biệt quan trọng trong các ứng dụng cần sự ổn định và độ tin cậy cao, ví dụ như các hệ thống quản lý cơ sở dữ liệu, các ứng dụng ngân hàng trực tuyến, hoặc các hệ thống điều khiển trực tuyến.
Khi một ngoại lệ xảy ra, chương trình có thể tạo ra thông báo lỗi có ý nghĩa, ghi lại thông tin chi tiết về sự cố, và thậm chí có thể phục hồi hoặc khôi phục trạng thái hệ thống.
Bên cạnh đó, việc xử lý ngoại lệ còn tăng cường khả năng bảo trì và phát triển chương trình. Khi các lỗi và sự cố được xử lý một cách hiệu quả và minh bạch, các lập trình viên có thể dễ dàng tìm ra nguyên nhân gốc rễ của vấn đề và phát triển các giải pháp và cải tiến một cách có hệ thống, giảm thiểu rủi ro và tăng hiệu suất phát triển.
3. Cơ chế Ngoại Lệ trong Java
Trong Java, chúng ta có thể sử dụng các cú pháp như throw
, try
, catch
và finally
để xử lý các Exception. Các từ khoá đó ứng với mỗi một giai đoạn trong quá trình phát hiện và xử lý exception.
Lưu ý, Java có phân biệt các kiểu ngoại lệ riêng biệt, mình sẽ đề cập đến các kiểu exception ở chương 4.
Dưới đây là giải thích từng bước về cơ chế Exception trong Java
3.1. Throw
Ở một số điều kiện cụ thể, chương trình sẽ thực hiện ném ra một ngoại lệ.
Ví dụ chúng ta phát triển một function có tên là divide
, function này nhận 2 tham số a
và b
, kết quả trả về là a/b
. Chương trình được viết như sau:
public static int divide(int a, int b) {
return a / b;
}
Chương trình trên sẽ gặp bất thường nếu số b = 0
, do phép chia không thể chia cho không. Đây là một exception.
Để xử lý exception này, chương trình của chúng ta cần xác định điều kiện cụ thể cho trường hợp đó, ở trường hợp này là khi b = 0
. Khi đó, chúng ta cần viết code như sau
public static int divide(int a, int b) throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException("Cannot divide by zero."); // throw exception ở đây
}
return a / b;
}
Có hai từ khoá mà các bạn cần lưu ý, là throws
ở dòng 1 và từ throw
ở dòng thứ ba.
throws
: từ khoá này được dùng để khai báo rằng function sẽ có thể ném ra ngoại lệ tương ứng. Mỗi một function có thể khai báo nhiều hơn 1 kiểu exception.
Khi function khai báo từ khoá throws, điều đó có nghĩa là trách nhiệm xử lý exception này thuộc calling method (phương thức gọi) hoặc một calling method xa hơn.
Kiểu ngoại lệ sẽ được đề cập ở chương sau.throw
: từ khoá này được sử dụng để tạo ra một ngoại lệ cụ thể và chuyển quyền kiểm soát từ phương thức hiện tại đến phương thức gọi (calling method) hoặc một khốicatch
tương ứng nếu có.
Tóm lại, 2 từ khoá throw
và throws
được dùng để khai báo exception có thể xảy ra tại một function, và chuyển hướng kiểm soát exception này đến calling method.
3.2. Try/catch
Khi called method thực hiện ném một exception, calling method sẽ có trách nhiệm xử lý exception đó. Calling method có thể xử lý trực tiếp, hoặc tiếp tục throw exception đó.
Tiếp tục ví dụ ở trên, giả sử chúng ta call đến function divide
một cách trực tiếp như sau
public static void main(String[] args) {
int result = divide(10, 0);
System.out.println(result);
}
Chương trình này sẽ không thể hoạt động được, do function divide
đã throw exception, nhưng function main
không thực hiện xử lý exception đó.
Dưới đây là đoạn code giúp function main
có thể xử lý được exception của function divide
.
public static void main(String[] args) {
try {
int result = divide(10, 0); // Might throw ArithmeticException
System.out.println(result);
} catch (ArithmeticException e) {
e.printStackTrace(); // Handle and log the exception
System.out.println(e.getMessage());
}
}
Có hai từ khoá mà các bạn cần lưu ý, là try
ở dòng thứ 2 và từ catch
ở dòng thứ năm.
try
: Khối try chứa đoạn mã có thể gặp lỗi hoặc ném ra ngoại lệ khi thực thi. Nếu có bất kỳ ngoại lệ nào xảy ra trong khối try, chương trình sẽ chuyển quyền kiểm soát đến khối catch tương ứng.
Mỗi khối try phải theo sau ít nhất một khối catch hoặc một khối finally.catch
: Khối catch chứa đoạn mã sẽ được thực thi nếu có ngoại lệ xảy ra trong khối try tương ứng. Khối catch chủ yếu được sử dụng để xử lý ngoại lệ hoặc ghi log về lỗi.
Có thể có nhiều khối catch sau một khối try, mỗi khối catch xử lý một loại ngoại lệ cụ thể.
3.3. Finally
Trong xử lý ngoại lệ của Java, từ khoá finally
được sử dụng để tạo một khối mã mà sẽ được thực thi sau khi thực hiện xong khối try
và khối catch
, không phụ thuộc vào việc có ngoại lệ xảy ra hay không. Điều này đảm bảo rằng mã trong khối finally luôn luôn được thực thi.
Quay trở lại ví dụ trên về phép chia cho số 0 ở trên, đoạn mã trên có thể được bổ sung như sau:
public static void main(String[] args) {
try {
int result = divide(10, 0); // Might throw ArithmeticException
System.out.println(result);
} catch (ArithmeticException e) {
e.printStackTrace(); // Handle and log the exception
System.out.println(e.getMessage());
}
finally {
System.out.println("Finally block executed.");
}
}
Xin lưu ý từ khoá finally
ở dòng 9, câu lệnh in ra dòng "Finally block executed." sẽ luôn được hiển thị ra bất chấp code của chúng ta có rơi vào exception hay không. Điều này cho phép chúng ta xử lý một số logic bắt buộc, ví dụ như dọn dẹp tài nguyên (đóng file sau khi mở, đóng kết nối cơ sở dữ liệu).
Finally là thành phần optional, nó không nhất thiết phải được sử dụng trong toàn bộ quá trình exception handling nếu không cần thiết.
4. Các loại Exception trong Java
Cả hai lớp Error và Exception đều kế thừa từ một lớp cha, có tên là Throwable.
Trong lớp Exception, chúng ta quan tâm đến 2 loại exception chính là RuntimeException, và các exception còn lại.
RuntimeException là exception mà chỉ có thể phát hiện được trong runtime, JVM sẽ bỏ qua các lỗi này ở compiler. Các lớp exception kế thừa lớp RuntimeException có tên gọi là Unchecked Exception.
Các other exception , kế thừa từ lớp exception được gọi là checked exception, do compiler phát hiện trong quá trình compile code. Do được phát hiện trong quá trình compile, các lập trình viên buộc phải xử lý try / catch các exception này.
4.1. Checked Exception
Checked Exceptions là những exception mà compiler kiểm tra tại thời điểm biên dịch.
Nếu một phương thức có khả năng phát sinh một Checked Exception, Java yêu cầu bạn hoặc phải xử lý ngoại lệ đó (sử dụng try-catch
) hoặc khai báo nó (sử dụng throws
), để rõ ràng cho bản thân và cho người khác biết rằng có một tình huống ngoại lệ có thể xảy ra.
Java sẽ không thể complier thành công nếu các Checked Exception không được xử lý hoàn toàn, do đó, chúng ta cần phải xử lý toàn bộ các checked exception trước khi complier.
Mục tiêu chính của Checked Exception là để thông báo cho lập trình viên về các vấn đề có thể xảy ra, và buộc lập trình viên phải tự quyết định cách xử lý các tình huống đó.
public static void main(String[] args) {
try {
FileInputStream file = new FileInputStream("nonexistentfile.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
Trong ví dụ trên, FileNotFoundException
là một Checked Exception. Do đó, Java buộc chúng ta phải xử lý ngoại lệ này (hoặc sử dụng try-catch
để bắt ngoại lệ hoặc sử dụng throws
để khai báo ngoại lệ).
Các loại checked exception tiêu biểu bao gồm:
- IOException:
Xuất hiện khi có lỗi nhập/xuất dữ liệu, ví dụ khi đọc/ghi file.
Các lớp con: FileNotFoundException, EOFException,
UnsupportedEncodingException v.v.
- ClassNotFoundException:
Xuất hiện trong quá trình complier, khi JVM không thể tìm thấy class được yêu cầu.
- SQLException:
Xuất hiện khi có lỗi trong quá trình truy vấn cơ sở dữ liệu
- InterruptedException:
Phát sinh khi một thread đang chờ, ngủ, hoặc bận rộn, và một thread khác đã làm gián đoạn nó.
- ReflectiveOperationException:
Là lớp cha của các ngoại lệ có thể phát sinh khi sử dụng API Reflection, như InvocationTargetException, InstantiationException, v.v.
- MalformedURLException:
Là một exception trong việc phân tích URL, nó sẽ kiểm tra URL có chính xác hay không.
- ParseException:
Xuất hiện khi có lỗi phân tích (parse) chuỗi sang một đối tượng, thường xuất hiện khi chuyển đổi chuỗi sang đối tượng Date hoặc số.
4.2. Unchecked Exception
Khác với checked exception có thể được JVM tìm ra và cảnh báo đến các lập trình viên, unchecked exception lại không được JVM phát hiện ra trong quá trình complier.
Tất cả các unchecked exception đều kế thừa từ lớp RuntimeException, và chỉ được phát hiện trong runtime, không được kiểm tra trong compile-time.
Compiler sẽ không báo lỗi hay cảnh báo nếu bạn không xử lý (ví dụ: không sử dụng try-catch
hoặc không khai báo throws
) các Unchecked Exceptions.
Các bạn chắc vẫn còn nhớ về ví dụ phép chia cho số 0 ở chương đầu.
public static int divide(int a, int b) {
return a / b;
}
Compiler sẽ không báo lỗi tại thời điểm biên dịch, nhưng khi bạn chạy chương trình, JVM sẽ ném ra ArithmeticException
tại runtime nếu b = 0
.
Các loại unchecked exception tiêu biểu bao gồm:
- ArithmeticException
Xảy ra khi có một phép toán không hợp lệ, ví dụ như chia một số cho zero.
- NullPointerException
Xảy ra khi chương trình cố gắng truy cập hoặc sử dụng một đối tượng mà thực sự là null.
- ArrayIndexOutOfBoundsException
Xảy ra khi chương trình cố gắng truy cập vào một chỉ số của mảng không tồn tại.
- ClassCastException
Xảy ra khi chương trình cố gắng ép kiểu (cast) một đối tượng sang một kiểu không tương thích.
- NumberFormatException
Xảy ra khi chương trình cố gắng chuyển đổi một chuỗi không phải là số sang một kiểu số (ví dụ: Integer.parseInt("abc")).
- IllegalStateException
Xảy ra khi một đối tượng không ở trong trạng thái thích hợp để thực hiện một hoạt động nào đó.
Nhìn chung, những Unchecked Exception này thường xuất phát từ các lỗi lập trình và thiết kế, và lập trình viên có trách nhiệm phát hiện và xử lý chúng để tránh các lỗi runtime có thể xảy ra.
Cá nhân mình nhận thấy đa phần lỗi xuất phát từ những Unchecked Exception này.
4.3. Error
Error là một lớp con của Throwable, nó ngang hàng với Exception. Do đó, về mặt bản chất nếu liệt kê Error là một Exception là không đúng. Tuy nhiên mình vẫn sẽ đề cập đến Error như một lỗi có thể xuất phát trong lúc code để cùng phân tích và xử lý.
Giống với Exception, Error cũng kế thừa lớp Throwable, nên chúng ta có thể dùng cú pháp try, catch
để bắt được error trong quá trình thực thi code.
Tuy nhiên, Error được xác định là một lớp riêng biệt với mục đích rất cụ thể: chúng ta không nên cố gắng bắt nó.
Theo tài liệu chính thống từ Oracle – công ty quản lý và phát triển phiên bản chính thức của Java, Error biểu thị một vấn đề nghiêm trọng xảy ra khi thực thi chương trình mà ta không nên cố gắng xử lý.
Đây là một tình trạng bất thường mà một ứng dụng không nên gặp phải.
An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions. The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it. (by Oracle)
Dưới đây là một số loại error phổ biến mà bạn có thể gặp trong Java:
- OutOfMemoryError: Xảy ra khi hệ thống không còn đủ bộ nhớ cho ứng dụng. Điển hình là khi bạn cố gắng cấp phát bộ nhớ mà JVM không thể cung cấp.
- StackOverflowError: Được gây ra bởi đệ quy quá sâu, dẫn đến kích thước của stack vượt quá giới hạn cho phép.
- NoClassDefFoundError: Xảy ra khi lớp đã được biên dịch, nhưng không thể được tìm thấy tại thời điểm chạy.
- UnknownError: Một lỗi không rõ nguyên nhân.
Đây chỉ là một số error phổ biến trong Java, và có nhiều error khác tồn tại. Điều quan trọng cần nhớ là, trong hầu hết các trường hợp, các lỗi này biểu thị các vấn đề mà ứng dụng không nên (hoặc không thể) xử lý và thường cần được giải quyết ở cấp độ hệ thống hoặc cấu hình.
5. Custom một Exception hoặc Error
Ngoài các error, exception có sẵn, Java cho phép chúng ta tạo thêm các error và exception để lập trình viên có thể tuỳ biến và sử dụng trong các mục đích khác nhau.
5.1. Custom một Exception
Để có thể tạo ra một exception tuỳ ý, đầu tiên, bạn tạo một lớp mới kế thừa từ Exception
với trường hợp checked exception, hoặc lớp RuntimeException
với trường hợp unchecked exception.
public class CustomException extends Exception {
public CustomException(String message) {
super(message);
}
// Bạn cũng có thể thêm các hàm khởi tạo (constructor) khác nếu cần thiết
}
Tiếp theo, chúng ta sẽ sử dụng CustomException
này như sau
public class SomeClass {
public void someMethod(int value) throws CustomException {
if (value < 0) {
throw new CustomException("Value không được âm");
}
// phần xử lý còn lại.
}
public static void main(String[] args) {
SomeClass obj = new SomeClass();
try {
obj.someMethod(-1);
} catch (CustomException e) {
e.printStackTrace();
}
}
}
Ở ví dụ trên, chúng ta đã tạo và sử dụng một class tên CustomException
. Function SomeMethod
đã khai báo throws CustomException
, vì thế function main
buộc phải xử lý catch exception
mà function SomeMethod
đã ném ra.
5.2. Custom một Error
Tương tự như việc tạo ra một exception tuỳ chỉnh, chúng ta cũng có thể tạo một error tuỳ chỉnh bằng cách tạo ra một class kế thừa classError
.
public class CustomError extends Error {
public CustomError(String message) {
super(message);
}
// Bạn cũng có thể thêm các hàm khởi tạo khác nếu cần thiết
}
Tuy nhiên, xin lưu ý rằng việc tạo và sử dụng lớp error tùy chỉnh không được khuyến khích, vì Error
thường dành cho các vấn đề nghiêm trọng mà JVM gặp phải và không nên được sử dụng trong mã ứng dụng thông thường.
6. Thực hành một số Exception thường gặp
6.1. Checked Exception
Dưới đây là một số ví dụ về việc sử dụng checked exception
6.1.1. Tìm kiếm file
Để có thể tìm kiếm vào trong một file, chúng ta sử dụng class FileReader
FileReader reader = new FileReader("somefile.txt");
Class FileReader
sẽ throw một exception là FileNotFoundException
Vì thế, khi chúng ta muốn tìm kiếm một file, chúng ta cần xử lý exception FileNotFoundException
.
Vì thế, code đầy đủ trong trường hợp này là:
public static void main(String[] args) {
try
{
FileReader reader = new FileReader("somefile.txt");
int ch;
while ((ch = reader.read()) != -1) {
System.out.print((char) ch);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (reader != null) {
reader.close();
}
}
}
6.1.2. Tìm một class
Để tim một class khác trong Java, chúng ta có thể dùng hàm Class.forName
Class<?> cls = Class.forName("NonExistentClass");
Function Class.forName
sẽ throw một exception tên là ClassNotFoundException
Do đó, chúng ta cần xử lý exception này khi sử dụng.
public static void findClass(String[] args) {
try {
Class<?> cls = Class.forName("NonExistentClass");
} catch (ClassNotFoundException e) {
// Chúng ta cần xử lý exception này trong mọi trường hợp.
e.printStackTrace();
}
}
6.2. Unchecked Exception
6.2.1. Kiểm tra giá trị null
Khi chúng ta đang cố truy cập vào một giá trị null, exception NullPointerException
sẽ được ném ra. Tuy nhiên do là unchecked exception, chúng ta có thể không cần catch exception này.
public static void checkString(String[] args) {
String str = null;
System.out.println(str.length()); // This will throw NullPointerException
}
6.2.2. Truy cập vào index không tồn tại
Khi chúng ta cố gắng truy cập đến một index không có trong một array, exception ArrayIndexOutOfBoundsException
sẽ được ném ra.
public static void checkArr(String[] args) {
int[] arr = {1, 2, 3};
System.out.println(arr[5]); // This will throw ArrayIndexOutOfBoundsException
}
6.3. Error
Thực tế, khi một Error
xảy ra, nó thường chỉ ra một vấn đề nghiêm trọng và ứng dụng thường nên được dừng lại và kiểm tra lại.
Do đó, bất cứ thời điểm và dòng code nào trong ứng dụng cũng có thể xuất hiện Error
, vì thế Error
mới khó đoán và khó xử lý.
Dưới đây là một số mô phỏng thực tế có thể xảy ra Error
.
6.3.1. Hết bộ nhớ
Khi hệ thống hết bộ nhớ, OutOfMemoryError
sẽ được ném ra. Chúng ta có thể mô phỏng lỗi này bằng cách cố gắng cấp phát một mảng lớn.
public class OutOfMemoryErrorExample {
public static void main(String[] args) {
long[] arr = new long[Integer.MAX_VALUE];
}
}
// Error OutOfMemorry sẽ xuất hiện khi chạy ứng dụng.
6.3.2. Tràn bộ đệm (stack overflow)
Khi độ sâu của ngăn xếp gọi vượt quá giới hạn cho phép, StackOverflowError
sẽ được ném ra. Một cách thông thường để gây ra lỗi này là thông qua đệ quy không có điều kiện dừng:
public class StackOverflowErrorExample {
public static void main(String[] args) {
recursiveMethod();
}
public static void recursiveMethod() {
recursiveMethod();
}
}
// Error StackOverflowError sẽ xuất hiện
7. Best practice về Exception trong Java
Khi làm việc với exception trong Java, việc tuân theo một số best practice sẽ giúp mã của bạn dễ đọc, dễ bảo trì và tránh được các lỗi tiềm ẩn. Một số cách triển khai thiếu hiệu quả sẽ có tác dụng ngược lại, vì thế hãy thận trọng khi sử dụng exception.
Dưới đây là một số best practice bạn nên theo dõi:
7.1. Sử dụng một Exception đúng nghĩa
Khi bạn sử dụng một loại exception không phù hợp với tình huống thực sự xảy ra. Giả sử bạn muốn kiểm tra giá trị tuổi của một người:
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new ArrayIndexOutOfBoundsException("Age is not valid!"); // Sử dụng exception ArrayIndex là sai, chúng ta đang kiểm tra số tuổi
}
this.age = age;
}
Việc sử dụng Exception sai với bối cảnh mà chúng ta gặp phải dễ dẫn đến các nhầm lẫn trong quá trình triển khai code sau này.
Ở ví dụ trên, chúng ta nên tạo một exception mới, có tên là InvalidAgeException
.
class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
public void setAge(int age) throws InvalidAgeException {
if (age < 0 || age > 150) {
throw new InvalidAgeException("Age is not valid!");
}
this.age = age;
}
Với trường hợp này, exception InvalidAgeException
rất rõ nghĩa, đúng bối cảnh sử dụng, tránh các nhầm lẫn không đáng có và gây ra hiểu nhầm không cần thiết.
7.2. Tránh việc "ăn" Exception - Eating Exceptions
Khi chúng ta bắt gặp một exception, chúng ta buộc phải xử lý nó. Đó là lý do mà exception tồn tại. "Ăn" exception là hành động ngược lại, chúng ta cố tình phớt lờ, bỏ qua nó, "ăn" nó khiến exception này dường như không tồn tại trong hệ thống.
Việc bỏ qua exception sẽ gây ra rất nhiều hệ luỵ, ví dụ như:
- Khó khăn trong việc xác định và sửa lỗi: Việc ẩn giấu một exception khiến công việc truy vết gặp rất nhiều khó khăn.
- Ứng dụng thiếu tính ổn định: Việc bỏ qua exception có thể dẫn tới việc xử lý các vấn đề logic bị sai phạm.
- Rủi ro về an ninh: Trong một số trường hợp, việc "giấu" exception có thể che giấu những lỗ hổng an ninh, khiến hệ thống trở nên dễ bị tấn công.
Và một vài vấn đề khác nữa.
Ví dụ về một trường hợp "ăn" exception
try {
// some code that may throw an exception
} catch (IOException e) {
// Empty catch block, silently "eating" the exception
}
Hiển nhiên việc không làm gì trong khối catch
là một hành động "ăn" exception, nó làm mất đi dấu vết về một vấn đề trong hệ thống.
Nếu không làm gì về mặt logic, ít nhất chúng ta cũng nên viết Log để hệ thống có thể truy vết trong trường hợp cần thiết.
try {
// some code that may throw an exception
} catch (IOException e) {
e.printStackTrace(); // Logging the exception
// Or handle the exception in a meaningful way
}
7.3. Sử dụng Checked Exception cho các tình huống cố gắng phục hồi
Bài toán giả định đặt ra là bạn đang muốn kết nối đến một cơ sở dữ liệu.
Sử dụng unchecked exception trong trường hợp này không bắt buộc người gọi phương thức phải xử lý ngoại lệ hoặc phải khai báo nó. Điều này dẫn đến việc người dùng hoặc ứng dụng có thể không biết về khả năng xảy ra ngoại lệ và không có cơ hội phục hồi từ sự cố đó.
public void connectToDatabase() {
if (!canConnect()) {
// Một khi kết nối không thành công, chúng ta không thể tìm cách kết nối lại.
throw new RuntimeException("Cannot connect to database.");
}
// Logic xử lý kết nối
}
Sử dụng checked exception (ví dụ: IOException
, SQLException
hoặc một exception tùy chỉnh) cho tình huống trên, ví dụ:
public void connectToDatabase() throws DatabaseConnectionException {
if (!canConnect()) {
throw new DatabaseConnectionException("Cannot connect to database.");
}
// Logic to connect to the database
}
Khi sử dụng checked exception, calling method bị ép buộc phải xử lý ngoại lệ. Điều này thông báo cho chúng ta về khả năng xảy ra ngoại lệ và buộc chúng ta phải có cơ chế phục hồi, ví dụ: bằng cách thử kết nối lại hoặc hiển thị thông báo lỗi cho người dùng cuối.
7.4. Viết rõ chi tiết trong thông điệp Exception
Việc đưa ra đúng exception đôi khi vẫn chưa đủ ngữ cảnh cho chúng ta, do đó việc viết một message chính xác cũng cần được đảm bảo.
Ví dụ về cách dùng không chính xác
public void divide(int a, int b) {
if (b == 0) {
// Error occurred là gì nhỉ ?
throw new ArithmeticException("Error occurred.");
}
int result = a / b;
// Logic
}
Việc viết ra một message lỗi là Error occurred rất chung chung, khó hiểu và vì thế, có thể làm chúng ta mất thời gian để hiểu toàn bộ thông tin lỗi.
Thay vào đó, một message tốt sẽ dễ dàng cho chúng ta biết được bối cảnh đang diễn ra.
public void divide(int a, int b) {
if (b == 0) {
// Quá rõ ràng, không cần phải giải thích thêm
throw new ArithmeticException("Division by zero attempted.");
}
int result = a / b;
// Logic
}
Kết luận về Exception
Exception, hay ngoại lệ, là một sự kiện bất thường xảy ra trong quá trình thực thi chương trình và có thể làm gián đoạn quá trình này. Các exception xuất phát từ nhiều nguyên nhân khác nhau, chẳng hạn như lỗi người dùng, lỗi phần cứng, hoặc lỗi logic trong mã nguồn.
Mục tiêu chính của việc sử dụng exception là cung cấp một cơ chế để phát hiện và xử lý các tình huống bất thường mà không làm gián đoạn toàn bộ chương trình. Hơn nữa, nó giúp mã nguồn trở nên gọn gàng, dễ đọc và dễ bảo trì hơn bằng cách tách biệt giữa logic chính của chương trình và logic xử lý lỗi.
Để triển khai exception một cách hiệu quả, người lập trình nên tuân theo một số nguyên tắc quan trọng.
- Sử dụng exception có ý nghĩa: Chọn loại exception phù hợp với tình huống cụ thể.
- Không "ăn" exception: Tránh việc bắt exception mà không xử lý hoặc không ghi log.
- Sử dụng checked exception cho các tình huống có thể phục hồi, và unchecked exception cho các lỗi nghiêm trọng không thể phục hồi.
- Cung cấp thông tin chi tiết trong thông điệp exception: Điều này giúp debug và tìm hiểu nguyên nhân của lỗi.
Exception là một công cụ mạnh mẽ trong lập trình, cho phép ta xử lý các tình huống không mong muốn một cách linh hoạt và hiệu quả. Khi được sử dụng đúng cách, exception không chỉ giúp mã nguồn trở nên sạch sẽ và dễ đọc hơn, mà còn tăng cường độ bền và ổn định của ứng dụng.
Tài liệu tham khảo:
- Exception in Java - Geeksforgeeks
- Exception handling - wiki
- Error - Oracle
- Throwable - Oracle
- Exception - Oracle
- Best practice to handle exception in Java - Geeksforgeeks
Exception trong Java đóng vai trò quan trọng trong việc xử lý lỗi và ngoại lệ, giúp quản lý lỗi một cách hiệu quả. Nắm vững cách sử dụng và xử lý exception là một kỹ năng cần thiết cho mọi lập trình viên Java.
Bài viết đã giới thiệu khái niệm, cách sử dụng và cách xử lý exception, đồng thời chia sẻ những lời khuyên quan trọng khi làm việc với lỗi và ngoại lệ. Nắm bắt điều này sẽ giúp bạn xây dựng ứng dụng Java ổn định và đáng tin cậy hơn. Hãy tiếp tục rèn luyện và áp dụng kiến thức này vào công việc lập trình hàng ngày của bạn nhé!
Bạn hãy thường xuyên theo dõi các bài viết hay về Lập Trình & Dữ Liệu trên 200Lab Blog nhé. Cũng đừng bỏ qua những khoá học Lập Trình tuyệt vời trên 200Lab nè.
Một vài bài viết mới bạn sẽ thích:
Git là gì? Tổng quan về Git cơ bản cho lập trình viên
Git là gì? Tổng quan về Git cơ bản cho lập trình viên
Bitbucket là gì? GitHub là gì? So sánh Bitbucket và GitHub
Cách giải quyết lỗi password authentication Github
Mobile & Web UI Kit For Flutter (100+ screens)
Bài viết liên quan
Giới thiệu Kiến trúc Backend for Frontend (BFF)
Nov 16, 2024 • 10 min read
Flask là gì? Hướng dẫn tạo Ứng dụng Web với Flask
Nov 15, 2024 • 7 min read
Webhook là gì? So sánh Webhook và API
Nov 15, 2024 • 8 min read
Spring Boot là gì? Hướng dẫn Khởi tạo Project Spring Boot với Docker
Nov 14, 2024 • 6 min read
Two-Factor Authentication (2FA) là gì? Vì sao chỉ Mật khẩu thôi là chưa đủ?
Nov 13, 2024 • 7 min read
Test-Driven Development (TDD) là gì? Hướng dẫn thực hành TDD
Nov 13, 2024 • 6 min read