Java cung cấp một cách tiếp cận mạnh mẽ và hướng đối tượng để xử lý các tình huống ngoại lệ, gọi là Java Exception Handling. Trước đây tôi đã có một bài viết chuyên sâu về chủ đề này, và hôm nay tôi sẽ tổng hợp một số câu hỏi quan trọng về Ngoại lệ Java cùng với lời giải đáp chi tiết, nhằm hỗ trợ bạn trong các buổi phỏng vấn.

1. Ngoại lệ trong Java là gì?
Một ngoại lệ là một sự kiện lỗi có thể xảy ra trong quá trình chạy chương trình, làm gián đoạn luồng hoạt động thông thường. Ngoại lệ có thể phát sinh từ nhiều tình huống khác nhau 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, v.v.
Bất cứ khi nào một lỗi xảy ra trong khi thực thi một câu lệnh Java, một đối tượng (object) ngoại lệ sẽ được tạo. Sau đó, JRE (Java Runtime Environment) cố gắng tìm một trình xử lý ngoại lệ phù hợp để xử lý ngoại lệ đó. Nếu tìm thấy, đối tượng ngoại lệ sẽ được chuyển đến code xử lý để thực hiện quy trình xử lý, đây được gọi là bắt ngoại lệ (catching the exception). Trong trường hợp không tìm thấy trình xử lý nào, ứng dụng sẽ đẩy ngoại lệ đó ra môi trường thực thi lệnh (runtime environment) và JRE sẽ chấm dứt chương trình.
Cần lưu ý rằng framework xử lý ngoại lệ của Java chỉ được sử dụng để xử lý các lỗi xảy ra trong thời gian chạy (runtime error), không phải các lỗi trong quá trình biên dịch (compile-time errors).
2. Các từ khóa xử lý ngoại lệ trong Java là gì?
Có bốn từ khóa được sử dụng trong xử lý ngoại lệ Java:
throw: Đôi khi, chúng ta chủ động tạo một object ngoại lệ và ném nó để dừng luồng xử lý bình thường của chương trình. Từ khóathrowđược dùng để chuyển ngoại lệ tới môi trường runtime để nó được xử lý.throws: Khi một phương thức ném ra một ngoại lệ đã được kiểm tra (checked exception) nhưng không tự xử lý, chúng ta cần dùng từ khóathrowstrong chữ ký phương thức. Điều này giúp chương trình gọi biết về các ngoại lệ mà phương thức có thể ném ra. Phương thức gọi có thể chọn xử lý các ngoại lệ này hoặc tiếp tục truyền chúng lên cấp gọi cao hơn bằng từ khóathrows. Chúng ta có thể liệt kê nhiều ngoại lệ trong mệnh đềthrows, và từ khóa này cũng có thể sử dụng với phương thứcmain().try-catch: Chúng ta sử dụng khốitry-catchđể xử lý ngoại lệ trong code của mình.trylà điểm bắt đầu của khối vàcatchnằm ở cuối khốitryđể xử lý các ngoại lệ. Một khốitrycó thể đi kèm với nhiều khốicatch, và các khốitry-catchcũng có thể được lồng vào nhau. Khốicatchyêu cầu một tham số phải là một kiểu ngoại lệ (ví dụ:Exception).finally: Khốifinallylà tùy chọn và chỉ có thể được sử dụng cùng với khốitry-catch. Vì ngoại lệ có thể làm gián đoạn quá trình thực thi, một số tài nguyên đang mở có thể không được đóng đúng cách. Khốifinallyđảm bảo rằng các tài nguyên này được giải phóng, vì mã trong khốifinallysẽ luôn được thực thi cho dù có ngoại lệ nào xảy ra hay không.
3. Giải thích hệ thống phân cấp ngoại lệ trong Java?
Trong Java, các ngoại lệ được tổ chức theo một hệ thống phân cấp và tính kế thừa chặt chẽ, nhằm phân loại các kiểu ngoại lệ khác nhau. Throwable là lớp cha của toàn bộ hệ thống phân cấp Ngoại lệ Java, và nó có hai lớp con trực tiếp là Error và Exception. Các ngoại lệ sau đó được chia thành hai loại chính: ngoại lệ được kiểm tra (Checked Exceptions) và ngoại lệ thời gian chạy (Runtime Exceptions).
- Errors là các tình huống ngoại lệ nằm ngoài khả năng kiểm soát của ứng dụng và không thể dự đoán hay phục hồi được. Ví dụ điển hình bao gồm lỗi phần cứng, sự cố JVM, hoặc tình trạng hết bộ nhớ.
- Checked Exceptions là những tình huống ngoại lệ mà chúng ta có thể dự đoán trong chương trình và có thể tìm cách phục hồi. Ví dụ như
FileNotFoundException. Đối với các ngoại lệ này, chúng ta nên phát hiện được chúng, sau đó cung cấp thông báo hữu ích cho người dùng và ghi log một cách đầy đủ cho mục đích sửa lỗi.Exceptionlà lớp cha của tất cả các Checked Exceptions. - Runtime Exceptions thường là kết quả của lỗi lập trình kém, chẳng hạn như việc cố gắng truy xuất một phần tử từ một mảng ở chỉ mục không hợp lệ. Ví dụ, chúng ta nên kiểm tra độ dài của mảng trước khi cố gắng truy xuất phần tử. Nếu không, nó có thể ném
ArrayIndexOutOfBoundsExceptiontại thời điểm thực thi.RuntimeExceptionlà lớp cha của tất cả các ngoại lệ thời gian chạy.

4. Các phương thức quan trọng của lớp Exception trong Java là gì?
Exception và tất cả các lớp con của nó không cung cấp bất kỳ phương thức riêng biệt nào. Thay vào đó, tất cả các phương thức đều được định nghĩa trong lớp cơ sở Throwable.
String getMessage(): Phương thức này trả về thông báo chuỗi củaThrowable. Thông báo này có thể được cung cấp khi tạo ngoại lệ thông qua hàm tạo của nó.String getLocalizedMessage(): Phương thức này được cung cấp để các lớp con có thể ghi đè, nhằm mục đích cung cấp các thông báo cụ thể theo ngôn ngữ (bản địa hóa) cho chương trình gọi. Việc triển khai phương thức này trong lớpThrowableđơn giản là sử dụng phương thứcgetMessage()để trả về thông báo ngoại lệ.synchronized Throwable getCause(): Phương thức này trả về nguyên nhân gốc rễ của ngoại lệ hoặcnullnếu nguyên nhân không xác định.String toString(): Phương thức này trả về thông tin vềThrowableở định dạng chuỗi. Chuỗi trả về chứa tên của lớpThrowablevà thông báo đã được bản địa hóa.void printStackTrace(): Phương thức này in thông tin dấu vết ngăn xếp (stack trace) ra luồng lỗi tiêu chuẩn. Phương thức này được nạp chồng (overloaded) và chúng ta có thể truyềnPrintStreamhoặcPrintWriterlàm đối số để ghi thông tin dấu vết ngăn xếp vào một tệp hoặc luồng cụ thể.
5. Giải thích tính năng ARM và khối multi-catch trong Java 7?
Nếu bạn đang bắt nhiều ngoại lệ trong một khối try duy nhất, bạn sẽ nhận thấy rằng code khối catch trông rất lộn xộn và chủ yếu bao gồm code dư thừa để ghi lại lỗi. Để giải quyết vấn đề này, một trong các tính năng của Java 7 là khối multi-catch được sử dụng để bắt nhiều ngoại lệ trong một khối catch duy nhất. Khối catch với tính năng này trông như sau:
catch(IOException | SQLException | Exception ex){
logger.error(ex);
throw new MyException(ex.getMessage());
}
Trong hầu hết các trường hợp, chúng ta sử dụng khối finally chỉ để đóng các tài nguyên. Đôi khi, nếu quên đóng, chúng ta có thể gặp phải các ngoại lệ thời gian chạy khi tài nguyên bị cạn kiệt. Những ngoại lệ này rất khó sửa lỗi, đòi hỏi chúng ta phải rà soát từng nơi sử dụng loại tài nguyên đó để đảm bảo rằng chúng đã được đóng. Vì lý do này, một trong những cải tiến của Java 7 là tính năng try-with-resources. Với tính năng này, chúng ta có thể 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 việc thực thi thoát khỏi khối try-catch, môi trường chạy sẽ tự động đóng các tài nguyên đó. Cấu trúc của khối try-catch với cải tiến này như sau:
try (MyResource mr = new MyResource()) {
System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
e.printStackTrace();
}
6. Sự khác biệt giữa Checked và Unchecked Exceptions trong Java là gì?
- Checked Exceptions cần được xử lý trong code bằng cách sử dụng khối
try-catch, hoặc phương thức phải khai báo chúng bằng từ khóathrowsđể thông báo cho phương thức gọi về các ngoại lệ có thể phát sinh. Ngược lại, Unchecked Exceptions không bắt buộc phải được xử lý trong chương trình hay khai báo trong mệnh đềthrowscủa phương thức. Exceptionlà lớp cha của tất cả các ngoại lệ được kiểm tra, trong khiRuntimeExceptionlà lớp cha của tất cả các ngoại lệ chưa kiểm tra. Cần lưu ý rằngRuntimeExceptionlà một lớp con củaException.- Các ngoại lệ được kiểm tra là những tình huống lỗi mà chương trình bắt buộc phải xử lý, nếu không sẽ dẫn đến lỗi tại thời điểm biên dịch. Ví dụ, khi sử dụng
FileReaderđể đọc một tệp, nó có thể némFileNotFoundException, và chúng ta phải bắt ngoại lệ này trong khốitry-catchhoặc ném lại cho phương thức gọi. Các ngoại lệ chưa kiểm tra thường do lỗi lập trình, chẳng hạn nhưNullPointerExceptionkhi gọi một phương thức trên một tham chiếu đối tượng mà không đảm bảo rằng nó không phải lànull. Ví dụ, tôi có thể viết một phương thức để loại bỏ tất cả nguyên âm khỏi một chuỗi. Trách nhiệm của người gọi là đảm bảo không truyền vào một chuỗinull. Mặc dù tôi có thể thay đổi phương thức để xử lý những tình huống này, nhưng lý tưởng nhất là người gọi nên tự xử lý chúng trước.
7. Sự khác biệt giữa từ khóa throw và throws trong Java là gì?
Từ khóa throws được sử dụng với chữ ký phương thức để khai báo các ngoại lệ mà phương thức có thể ném. Còn từ khóa throw được sử dụng để làm gián đoạn luồng của chương trình và chuyển object ngoại lệ cho runtime để xử lý nó.
8. Cách viết các ngoại lệ tùy chỉnh trong Java?
Chúng ta có thể tạo lớp ngoại lệ tùy chỉnh bằng cách mở rộng lớp Exception hoặc bất kỳ lớp con nào của nó. Lớp ngoại lệ tùy chỉnh này có thể chứa các biến và phương thức riêng, cho phép chúng ta truyền mã lỗi hoặc các thông tin liên quan khác đến trình xử lý ngoại lệ. Một ví dụ đơn giản về ngoại lệ tùy chỉnh được minh họa như dưới đây.
package com.journaldev.exceptions;
import java.io.IOException;
public class MyException extends IOException {
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;
}
}
9. OutOfMemoryError trong Java là gì?
OutOfMemoryError trong Java là một lớp con của java.lang.VirtualMachineError. Nó được ném bởi JVM khi nó hết bộ nhớ heap. Chúng ta có thể khắc phục lỗi này bằng cách cung cấp thêm bộ nhớ để chạy ứng dụng java thông qua các tùy chọn java.
$>java MyProgram -Xms1024m -Xmx1024m -XX:PermSize=64M -XX:MaxPermSize=256m
10. Các tình huống khác nhau gây ra “Exception in thread main”?
Một số tình huống ngoại lệ phổ biến là:
- Exception in thread main java.lang.UnsupportedClassVersionError: Ngoại lệ này xảy ra khi lớp java của bạn được biên dịch từ một phiên bản JDK khác và bạn đang cố gắng chạy nó từ một phiên bản java khác.
- Exception in thread main java.lang.NoClassDefFoundError: Có hai biến thể của ngoại lệ này. Biến thể thứ nhất là khi bạn cung cấp tên đầy đủ của lớp với phần mở rộng .class. Tình huống thứ hai là khi Class không được tìm thấy.
- Exception in thread main java.lang.NoSuchMethodError: main: Ngoại lệ này xảy ra khi bạn đang cố gắng chạy một lớp không có phương thức main.
- Exception in thread “main” java.lang.ArithmeticException: Bất cứ khi nào một ngoại lệ được ném từ phương thức main, nó sẽ in ngoại lệ trong console. Phần đầu tiên giải thích rằng một ngoại lệ được ném từ phương thức main, phần thứ hai in tên lớp ngoại lệ và sau dấu hai chấm, nó in thông báo ngoại lệ.
11. Sự khác biệt giữa final, finally và finalize trong Java là gì?
final và finally là các từ khóa trong Java, còn finalize là một phương thức.
- Từ khóa
finalđược sử dụng với:- Biến: Đảm bảo giá trị của biến không thể bị gán lại sau khi khởi tạo.
- Lớp: Ngăn chặn các lớp khác kế thừa.
- Phương thức: Chặn việc ghi đè (override) bởi các lớp con.
- Từ khóa
finallyđược dùng với khốitry-catchđể cung cấp các câu lệnh luôn luôn được thực thi, bất kể có ngoại lệ xảy ra hay không.finallythường được sử dụng để đóng các tài nguyên đã mở. - Phương thức
finalize()được Trình dọn rác (Garbage Collector) thực thi ngay trước khi một đối tượng bị hủy. Đây từng được coi là một cách để đảm bảo tất cả các tài nguyên toàn cục được giải phóng.
Trong ba khái niệm này, chỉ có finally trực tiếp liên quan đến việc xử lý ngoại lệ trong Java.
12. Điều gì xảy ra khi một ngoại lệ được ném bởi phương thức main?
Khi một ngoại lệ được ném bởi phương thức main(), Java Runtime sẽ kết thúc chương trình và in thông báo ngoại lệ và stack trace trong console hệ thống.
13. Có thể có một khối catch trống không?
Có thể có một khối catch trống nhưng lý do là vì lập trình kém. Chúng ta không bao giờ nên có một khối catch trống vì nếu ngoại lệ được bắt bởi khối đó, ta sẽ không có thông tin về ngoại lệ và sẽ rất khó để sửa lỗi. Ít nhất phải có một câu lệnh ghi log để ghi lại chi tiết ngoại lệ trong console hoặc các tệp log.
14. Cung cấp một số phương pháp xử lý ngoại lệ tốt nhất trong Java?
Một số phương pháp tốt nhất để xử lý ngoại lệ Java là:
- Để dễ dàng gỡ lỗi, hãy sử dụng các ngoại lệ càng cụ thể càng tốt.
- Thực hiện “Fail-Fast” (ném ngoại lệ sớm) trong chương trình của bạn khi phát hiện lỗi.
- Chỉ bắt ngoại lệ ở cấp độ cao hơn (bắt muộn) trong chương trình, để phương thức gọi có thể xử lý chúng.
- Sử dụng tính năng ARM (Automatic Resource Management) của Java 7 để đảm bảo các tài nguyên được đóng tự động, hoặc sử dụng khối
finallyđể đóng chúng một cách rõ ràng. - Luôn luôn ghi lại (log) thông báo ngoại lệ đầy đủ cho mục đích gỡ lỗi.
- Tận dụng khối
multi-catchđể làm cho code của bạn gọn gàng và dễ đọc hơn. - Sử dụng các ngoại lệ tùy chỉnh để chỉ ném một loại ngoại lệ duy nhất từ API ứng dụng của bạn, giúp việc xử lý nhất quán hơn.
- Tuân thủ quy ước đặt tên cho các lớp ngoại lệ tùy chỉnh, luôn kết thúc bằng “Exception”.
- Ghi lại các ngoại lệ mà một phương thức có thể ném ra bằng cách sử dụng
@throwstrong Javadoc. - Việc tạo ngoại lệ tốn chi phí, vì vậy chỉ ném ngoại lệ khi thực sự có ý nghĩa. Nếu không, bạn có thể cân nhắc bắt chúng và trả về giá trị
nullhoặc một object rỗng.
15. Vấn đề với các chương trình dưới đây là gì và cách khắc phục?
Trong phần này, chúng ta sẽ xem thêm về một số câu hỏi lập trình liên quan đến các ngoại lệ java.
1. Vấn đề với chương trình dưới đây là gì?
package com.journaldev.exceptions;
import java.io.FileNotFoundException;
import java.io.IOException;
public class TestException {
public static void main(String[] args) {
try {
testExceptions();
} catch (FileNotFoundException | IOException e) {
e.printStackTrace();
}
}
public static void testExceptions() throws IOException, FileNotFoundException {
}
}
Chương trình trên không biên dịch và bạn sẽ nhận được thông báo lỗi là “The exception FileNotFoundException is already caught by the alternative IOException”. Điều này là do FileNotFoundException là một lớp con của IOException, có hai cách để giải quyết vấn đề này. Cách đầu tiên là sử dụng một khối catch duy nhất cho cả hai ngoại lệ.
try {
testExceptions();
} catch(FileNotFoundException e){
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Cách khác là loại bỏ FileNotFoundException khỏi khối multi-catch.
try {
testExceptions();
} catch (IOException e) {
e.printStackTrace();
}
Bạn có thể chọn một trong hai cách tiếp cận này dựa trên code khối catch của bạn.
2. Vấn đề với chương trình dưới đây là gì?
package com.journaldev.exceptions;
import java.io.FileNotFoundException;
import java.io.IOException;
import javax.xml.bind.JAXBException;
static class TestException1 {
public static void main(String[] args) {
try {
go();
} catch (IOException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (JAXBException e) {
e.printStackTrace();
}
}
public static void go() throws IOException, JAXBException, FileNotFoundException {
}
}
Chương trình sẽ không biên dịch được. Nguyên nhân là do FileNotFoundException là một lớp con của IOException. Khi bạn đặt khối catch cho FileNotFoundException sau IOException, khối catch cho FileNotFoundException sẽ không thể truy cập được, và bạn sẽ nhận được thông báo lỗi tương tự như: “Unreachable catch block for FileNotFoundException. It is already handled by the catch block for IOException”
(Tạm dịch: Khối catch không thể truy cập được cho FileNotFoundException. Nó đã được xử lý bởi khối catch cho IOException). Để khắc phục vấn đề này, bạn cần sắp xếp lại thứ tự các khối catch.”
try {
go();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (JAXBException e) {
e.printStackTrace();
}
Lưu ý rằng JAXBException không liên quan đến IOException hoặc FileNotFoundException và có thể được đặt ở bất kỳ đâu trong hệ thống phân cấp khối catch ở trên.
3. Vấn đề với chương trình dưới đây là gì?
package com.journaldev.exceptions;
import java.io.IOException;
import javax.xml.bind.JAXBException;
public class TestException2 {
public static void main(String[] args) {
try {
foo();
} catch (IOException e) {
e.printStackTrace();
} catch(JAXBException e){
e.printStackTrace();
} catch(NullPointerException e){
e.printStackTrace();
} catch(Exception e){
e.printStackTrace();
}
}
public static void bar(){
}
public static void foo() throws NullPointerException{
}
}
Đây là một câu hỏi đánh lừa, không có vấn đề gì với code này và nó sẽ biên dịch thành công. Ta luôn có thể bắt một Exception hoặc bất kỳ ngoại lệ chưa kiểm tra nào ngay cả khi nó không có trong mệnh đề throws của phương thức. Tương tự, nếu một phương thức (foo) khai báo một ngoại lệ chưa kiểm tra trong mệnh đề throws, không bắt buộc phải xử lý nó trong chương trình.
5. Vấn đề với chương trình dưới đây là gì?
package com.journaldev.exceptions;
import java.io.IOException;
public class TestException4 {
public void start() throws IOException{
}
public void foo() throws NullPointerException{
}
}
class TestException5 extends TestException4{
public void start() throws Exception{
}
public void foo() throws RuntimeException{
}
}
Chương trình trên sẽ không biên dịch vì chữ ký phương thức start() không giống nhau trong lớp con. Để khắc phục vấn đề này, chúng ta có thể thay đổi chữ ký phương thức trong lớp con để giống hệt với lớp cha hoặc chúng ta có thể loại bỏ throws khỏi phương thức lớp con như được hiển thị bên dưới.
@Override
public void start(){
}
6. Vấn đề với chương trình dưới đây là gì?
package com.journaldev.exceptions;
import java.io.IOException;
import javax.xml.bind.JAXBException;
public class TestException6 {
public static void main(String[] args) {
try {
foo();
} catch (IOException | JAXBException e) {
e = new Exception("");
e.printStackTrace();
} catch(Exception e){
e = new Exception("");
e.printStackTrace();
}
}
public static void foo() throws IOException, JAXBException{
}
}
Chương trình trên sẽ không thể biên dịch được. Lý do là vì đối tượng ngoại lệ trong khối multi-catch được coi là final, và chúng ta không thể gán lại giá trị cho nó. Bạn sẽ gặp lỗi biên dịch với thông báo: “The parameter e of a multi-catch block cannot be assigned”. Để khắc phục lỗi này, chúng ta cần loại bỏ việc gán lại đối tượng ngoại lệ mới cho biến e.
Đó là tất cả những câu hỏi phỏng vấn về ngoại lệ trong Java, hy vọng bạn thấy chúng hữu ích. Tôi sẽ tiếp tục bổ sung thêm vào danh sách này trong tương lai, vì vậy hãy đảm bảo bạn lưu lại trang này để tiện tham khảo sau này nhé.