- Sản phẩm & Dịch vụSản phẩm & Dịch vụ
- Giải phápGiải pháp
- Bảng giáBảng giá
- Công tyCông ty
- Tài liệuTài liệu
vi
vi
Chuyên mục với các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.
Technical Writer
I have over 5 years of experience writing technical documentation for tech products, making them accessible and user-friendly. My focus is always on providing clear and precise information. @#@ Tôi đã có hơn 5 năm kinh nghiệm viết tài liệu kỹ thuật cho các sản phẩm công nghệ, giúp người dùng dễ dàng tiếp cận và sử dụng. Tôi luôn tập trung vào việc cung cấp thông tin chính xác và dễ hiểu.
Thảo luận (0)
Đăng nhập để thảo luận
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.

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).
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óa throw đượ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óa throws trong 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óa throws. 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ức main().try-catch: Chúng ta sử dụng khối try-catch để xử lý ngoại lệ trong code của mình. trylà điểm bắt đầu của khối và catch nằm ở cuối khối try để xử lý các ngoại lệ. Một khối try có thể đi kèm với nhiều khối catch, và các khối try-catch cũng có thể được lồng vào nhau. Khối catch yêu cầu một tham số phải là một kiểu ngoại lệ (ví dụ: Exception).finally: Khối finally là tùy chọn và chỉ có thể được sử dụng cùng với khối try-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ối finally đảm bảo rằng các tài nguyên này được giải phóng, vì mã trong khối finally sẽ luôn được thực thi cho dù có ngoại lệ nào xảy ra hay không.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).
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. Exception là lớp cha của tất cả các Checked Exceptions.ArrayIndexOutOfBoundsException tại thời điểm thực thi. RuntimeException là lớp cha của tất cả các ngoại lệ thời gian chạy.
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ủa Throwable. 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ớp Throwable đơn giản là sử dụng phương thức getMessage() để 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ặc null nế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ớp Throwable và 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ền PrintStream hoặc PrintWriter là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ể.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();
}
try-catch, hoặc phương thức phải khai báo chúng bằng từ khóa throws để 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 đề throws của phương thức.Exception là lớp cha của tất cả các ngoại lệ được kiểm tra, trong khi RuntimeException là lớp cha của tất cả các ngoại lệ chưa kiểm tra. Cần lưu ý rằng RuntimeException là một lớp con của Exception.FileReader để đọc một tệp, nó có thể ném FileNotFoundException, và chúng ta phải bắt ngoại lệ này trong khối try-catch hoặ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ư NullPointerException khi 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ỗi null. 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.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ó.
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;
}
}
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
Một số tình huống ngoại lệ phổ biến là:
final và finally là các từ khóa trong Java, còn finalize là một phương thức.
final được sử dụng với:
finally được dùng với khối try-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. finally thường được sử dụng để đóng các tài nguyên đã mở.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.
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.
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.
Một số phương pháp tốt nhất để xử lý ngoại lệ Java là:
finally để đóng chúng một cách rõ ràng.multi-catch để làm cho code của bạn gọn gàng và dễ đọc hơn.@throws trong Javadoc.null hoặc một object rỗng.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.
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.
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.
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.
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(){
}
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é.