Xử lý ngoại lệ với ghi đè phương thức trong Java (Exception Overriding)
Trong lập trình Java, khi ghi đè (overriding) phương thức của lớp cha (superclass), việc xử lý ngoại lệ có một số quy tắc quan trọng cần lưu ý.
Các quy tắc:
↳ Quy tắc 1: Nếu phương thức lớp cha không khai báo ngoại lệ.
↳ Quy tắc 2: Nếu phương thức lớp cha khai báo ngoại lệ.
Để hiểu rõ hơn về quy tắc này, chúng ta hãy đi sâu vào từng khái niệm và ví dụ cụ thể nhé.
Quy tắc 1: Nếu phương thức lớp cha không khai báo ngoại lệ.
Nếu một phương thức trong lớp cha không khai báo bất kỳ checked exception nào, thì khi bạn ghi đè phương thức đó trong lớp con, bạn cũng không được phép khai báo thêm checked exception mới. Tuy nhiên, bạn vẫn có thể khai báo unchecked exception.
Tại sao lại có quy tắc này?
↳ Bảo đảm tính tương thích: Nếu lớp con có thể tùy ý khai báo checked exception mới, điều này sẽ làm cho việc sử dụng các đối tượng của lớp con trở nên khó dự đoán hơn. Các lập trình viên khác khi sử dụng lớp con sẽ phải luôn đề phòng những ngoại lệ mới có thể xảy ra, gây khó khăn trong việc viết code.
↳ Tránh làm hỏng hệ thống: Checked exception thường đại diện cho những lỗi nghiêm trọng có thể làm gián đoạn quá trình thực thi của chương trình. Nếu lớp con tự ý ném ra checked exception mới, điều này có thể dẫn đến việc chương trình bị dừng đột ngột nếu không được xử lý đúng cách.
Trường hợp 1: Lớp cha không khai báo ngoại lệ nhưng lớp con ghi đè phương thức và khai báo ngoại lệ được kiểm tra (checked exceptions).
Dưới đây là một ví dụ về lỗi biên dịch khi lớp con ghi đè phương thức và khai báo ngoại lệ được kiểm tra trong khi lớp cha không khai báo ngoại lệ được kiểm tra (checked exceptions):
Ví dụ: Example.java
import java.io.IOException;
// Lớp cha không khai báo ngoại lệ
class Parent {
public void display() {
System.out.println("Phương thức display() của lớp Parent");
}
}
// Lớp con ghi đè phương thức và khai báo ngoại lệ được kiểm tra
class Child extends Parent {
@Override
public void display() throws IOException {
System.out.println("Phương thức display() của lớp Child");
throw new IOException("IOException trong phương thức display() của lớp Child");
}
}
// Lớp chính để kiểm tra
public class Exmple {
public static void main(String[] args) {
Parent p = new Child();
try {
p.display();
} catch (IOException e) {
System.out.println("Ngoại lệ đã được bắt: " + e.getMessage());
}
}
}
Kết quả của chương trình là:
Khi bạn cố gắng biên dịch mã này, bạn sẽ nhận được lỗi biên dịch. Lỗi này xảy ra vì phương thức display() trong lớp Parent không khai báo ngoại lệ được kiểm tra (IOException), nhưng phương thức display() ghi đè trong lớp Child lại khai báo ngoại lệ đó.
Trường hợp 2: Lớp cha không khai báo ngoại lệ nhưng lớp con ghi đè phương thức và khai báo ngoại lệ không được kiểm tra (unchecked exceptions).
Nếu lớp con ghi đè phương thức và khai báo ngoại lệ không được kiểm tra (unchecked exception) trong Java, thì không có vấn đề gì cả. Điều này được phép vì ngoại lệ không được kiểm tra (unchecked exceptions), chẳng hạn như các lớp con của RuntimeException, không bắt buộc phải khai báo hoặc xử lý. Các quy tắc nghiêm ngặt về ngoại lệ chỉ áp dụng cho ngoại lệ được kiểm tra (checked exceptions).
Dưới đây là một ví dụ khi lớp con ghi đè phương thức và khai báo ngoại lệ được kiểm tra trong khi lớp cha không khai báo ngoại lệ không được kiểm tra (unchecked exceptions):
Ví dụ: Example.java
// Lớp cha không khai báo ngoại lệ
class Parent {
public void display() {
System.out.println("Phương thức display() của lớp Parent");
}
}
// Lớp con ghi đè phương thức và khai báo ngoại lệ không được kiểm tra
class Child extends Parent {
@Override
public void display() throws RuntimeException {
System.out.println("Phương thức display() của lớp Child");
throw new RuntimeException("RuntimeException trong phương thức display() của lớp Child");
}
}
// Lớp chính
public class Example {
public static void main(String[] args) {
Parent p = new Child();
try {
p.display();
} catch (RuntimeException e) {
System.out.println("Ngoại lệ đã được bắt: " + e.getMessage());
}
}
}
Kết quả của chương trình là:
Ngoại lệ đã được bắt: RuntimeException trong phương thức display() của lớp Child
Trong ví dụ này:
↳ Khi ghi đè phương thức, lớp con có thể khai báo và ném ra ngoại lệ không được kiểm tra (unchecked exceptions) như RuntimeException.
↳ Điều này không gây ra vấn đề gì vì ngoại lệ không được kiểm tra không bắt buộc phải khai báo hoặc xử lý.
↳ Điều này cho phép các lập trình viên linh hoạt hơn trong việc xử lý các tình huống lỗi không mong đợi mà không làm phức tạp hóa giao diện phương thức của lớp cha.
Quy tắc 2: Nếu phương thức lớp cha khai báo ngoại lệ.
Khi phương thức của lớp cha đã khai báo một ngoại lệ nào đó, thì phương thức được ghi đè trong lớp con có thể:
↳ Khai báo cùng loại ngoại lệ: Điều này có nghĩa là phương thức trong lớp con cũng sẽ ném ra ngoại lệ giống hệt như lớp cha.
↳ Khai báo ngoại lệ là con của ngoại lệ trong lớp cha: Tức là phương thức trong lớp con có thể ném ra một ngoại lệ cụ thể hơn, là một lớp con của ngoại lệ được khai báo trong lớp cha.
↳ Không khai báo ngoại lệ: Lớp cha khai báo một ngoại lệ, nhưng lớp con không khai báo hoặc không xử lý ngoại lệ:
↳ Quan trọng: Phương thức trong lớp con không được phép khai báo ngoại lệ là cha của ngoại lệ trong lớp cha.
Trường hợp 1: Phương thức trong lớp con ghi đè phương thức của lớp cha và khai báo cùng loại ngoại lệ mà phương thức của lớp cha đã khai báo.
Trong trường hợp này, bạn có thể thấy rằng phương thức của lớp con khai báo cùng loại ngoại lệ với phương thức của lớp cha, điều này hoàn toàn hợp lệ và giúp người gọi phương thức nhận thức về các ngoại lệ mà phương thức có thể ném ra.
Ví dụ: Example.java
import java.io.IOException;
// Lớp cha
class Parent {
// Phương thức khai báo ngoại lệ IOException
public void display() throws IOException {
System.out.println("Phương thức display() của lớp Parent");
throw new IOException("IOException từ lớp Parent");
}
}
// Lớp con ghi đè phương thức display() của lớp cha
class Child extends Parent {
@Override
public void display() throws IOException {
System.out.println("Phương thức display() của lớp Child");
throw new IOException("IOException từ lớp Child");
}
}
public class Example {
public static void main(String[] args) {
Parent p = new Child();
try {
p.display();
} catch (IOException e) {
System.out.println("Ngoại lệ đã được bắt: " + e.getMessage());
}
}
}
Kết quả của chương trình là:
Ngoại lệ đã được bắt: IOException từ lớp Child
Trong ví dụ này, phương thức ghi đè trong lớp con khai báo cùng loại ngoại lệ (IOException) như phương thức của lớp cha. Điều này hoàn toàn hợp lệ theo quy tắc 2. Khi bạn chạy chương trình, phương thức display() của lớp Child sẽ được gọi và sẽ ném ra ngoại lệ IOException. Khối catch trong phương thức main sẽ bắt và xử lý ngoại lệ đó.
Trường hợp 2: Phương thức trong lớp con có thể ném ra một ngoại lệ cụ thể hơn, là một lớp con của ngoại lệ được khai báo trong lớp cha.
Trong trường hợp này, bạn có thể thấy rằng phương thức của lớp con khai báo một ngoại lệ cụ thể hơn so với phương thức của lớp cha, điều này giúp làm rõ và cụ thể hóa các lỗi có thể xảy ra trong phương thức của lớp con.
Ví dụ: Example.java
import java.io.FileNotFoundException;
import java.io.IOException;
// Lớp cha
class Parent {
// Phương thức khai báo ngoại lệ IOException
public void display() throws IOException {
System.out.println("Phương thức display() của lớp Parent");
throw new IOException("IOException từ lớp Parent");
}
}
// Lớp con ghi đè phương thức display() của lớp cha
class Child extends Parent {
@Override
// Ngoại lệ FileNotFoundException một lớp con của Ngoại lệ IOException.
public void display() throws FileNotFoundException {
System.out.println("Phương thức display() của lớp Child");
throw new FileNotFoundException("FileNotFoundException từ lớp Child");
}
}
public class Example {
public static void main(String[] args) {
Parent p = new Child();
try {
p.display();
} catch (IOException e) {
System.out.println("Ngoại lệ đã được bắt: " + e.getMessage());
}
}
}
Kết quả của chương trình là:
Ngoại lệ đã được bắt: FileNotFoundException từ lớp Child
Trong ví dụ này, phương thức ghi đè trong lớp con khai báo một ngoại lệ cụ thể hơn (FileNotFoundException) so với ngoại lệ được khai báo trong phương thức của lớp cha (IOException). Điều này hoàn toàn hợp lệ theo quy tắc 2 của ghi đè phương thức. Khi bạn chạy chương trình, phương thức display() của lớp Child sẽ được gọi và ném ra ngoại lệ FileNotFoundException. Khối catch trong phương thức main sẽ bắt và xử lý ngoại lệ này, vì FileNotFoundException là một lớp con của IOException.
Trường hợp 3: Lớp cha khai báo một ngoại lệ, nhưng lớp con không khai báo hoặc không xử lý ngoại lệ.
Khi phương thức lớp cha khai báo một checked exception:
↳ Phương thức ghi đè trong lớp con có thể không khai báo bất kỳ checked exception nào. Điều này không gây lỗi biên dịch và là trường hợp hợp lệ.
↳ Phương thức lớp con không cần phải xử lý checked exception từ lớp cha nếu nó không khai báo ném ra ngoại lệ.
Ví dụ: Example.java
import java.io.IOException;
// Lớp cha
class Parent {
public void display() throws IOException {
System.out.println("Phương thức display() của lớp Parent");
// Giả sử có thể xảy ra IOException
}
}
// Lớp con ghi đè phương thức display() của lớp cha
class Child extends Parent {
@Override
public void display() {
// Không khai báo ngoại lệ, cũng không xử lý
System.out.println("Phương thức display() của lớp Child");
// Không xử lý IOException từ lớp Parent
}
}
// Lớp chính để kiểm tra
public class Example {
public static void main(String[] args) {
try {
Parent p = new Child();
p.display(); // Gọi phương thức display() của lớp Child
} catch (IOException e) {
System.out.println("Ngoại lệ không được xử lý: " + e.getMessage());
}
}
}
Kết quả của chương trình là:
Ví dụ này phù hợp với quy tắc rằng khi phương thức lớp cha khai báo một checked exception, phương thức ghi đè trong lớp con có thể không khai báo hoặc xử lý checked exception đó. Điều này chứng minh rằng phương thức con không cần phải khai báo hoặc xử lý ngoại lệ từ phương thức lớp cha.
Trường hợp 4: Phương thức ghi đè trong lớp con khai báo ném ra một ngoại lệ cha của ngoại lệ được khai báo trong lớp cha.
Không thể khai báo ném ra loại ngoại lệ cha của ngoại lệ được khai báo trong lớp cha (ngoại lệ cha có thể lớn hơn, tức là bao phủ nhiều loại ngoại lệ hơn). Trường hợp này sẽ gây ra lỗi biên dịch.
Ví dụ: Example.java
import java.io.IOException;
// Lớp cha
class Parent {
public void display() throws IOException {
System.out.println("Phương thức display() của lớp Parent");
}
}
// Lớp con ghi đè phương thức display() của lớp cha
class Child extends Parent {
@Override
public void display() throws Exception { // Đây là lỗi
System.out.println("Phương thức display() của lớp Child");
}
}
// Lớp chính để kiểm tra
public class Example {
public static void main(String[] args) {
try {
Parent p = new Child();
p.display(); // Gọi phương thức display() của lớp Child
} catch (IOException e) {
System.out.println("Ngoại lệ không được xử lý: " + e.getMessage());
} catch (Exception e) {
System.out.println("Ngoại lệ không được xử lý: " + e.getMessage());
}
}
}
Kết quả của chương trình là:
Đoạn mã trên sẽ gây ra lỗi biên dịch vì phương thức display() trong lớp con khai báo ném ra một ngoại lệ (Exception) lớn hơn ngoại lệ được khai báo trong lớp cha (IOException).
Trường hợp 5: Phương thức ghi đè khai báo ném ra một ngoại lệ không phù hợp với kiểu của ngoại lệ khai báo trong lớp cha.
Nếu phương thức ghi đè khai báo ném ra một ngoại lệ không phù hợp với kiểu của ngoại lệ khai báo trong lớp cha, hoặc loại ngoại lệ không được kiểm tra, thì có thể gây ra lỗi biên dịch.
Ví dụ: Example.java
import java.io.IOException;
import java.sql.SQLException;
// Lớp cha
class Parent {
public void display() throws SQLException {
System.out.println("Phương thức display() của lớp Parent");
}
}
// Lớp con ghi đè phương thức display() của lớp cha
class Child extends Parent {
@Override
public void display() throws IOException { // Đây là lỗi vì IOException không phải là con của SQLException
System.out.println("Phương thức display() của lớp Child");
}
}
// Lớp chính để kiểm tra
public class Example {
public static void main(String[] args) {
Parent p = new Child();
try {
p.display(); // Gọi phương thức display() của lớp Child
} catch (SQLException e) {
System.out.println("Ngoại lệ không được xử lý: " + e.getMessage());
} catch (IOException e) {
System.out.println("Ngoại lệ không được xử lý: " + e.getMessage());
}
}
}
Kết quả của chương trình là:
Trong ví dụ này, Phương thức display() khai báo ném ra IOException, là một checked exception nhưng không phải là lớp con của SQLException. Điều này không phù hợp với quy tắc ghi đè phương thức và sẽ gây lỗi biên dịch.