Làm chủ kế thừa Java: Cách hoạt động của từ khóa extends (Hướng dẫn toàn diện)

1. Giới thiệu

Java là một ngôn ngữ lập trình được sử dụng rộng rãi trong nhiều lĩnh vực, từ hệ thống doanh nghiệp đến ứng dụng web và phát triển Android. Trong số nhiều tính năng của nó, “kế thừa” là một trong những khái niệm thiết yếu nhất khi học lập trình hướng đối tượng.

Bằng cách sử dụng kế thừa, một lớp mới (lớp con/lớp con) có thể kế thừa chức năng của một lớp hiện có (lớp cha/lớp cha). Điều này giúp giảm trùng lặp mã và làm cho chương trình dễ dàng mở rộng và bảo trì hơn. Trong Java, kế thừa được thực hiện bằng từ khóa extends.

Trong bài viết này, chúng tôi giải thích rõ ràng vai trò của từ khóa extends trong Java, cách sử dụng cơ bản, ứng dụng thực tế và các câu hỏi phổ biến. Hướng dẫn này hữu ích không chỉ cho người mới bắt đầu Java mà còn cho những người muốn ôn lại kế thừa. Đến cuối, bạn sẽ hiểu đầy đủ về ưu và nhược điểm của kế thừa cũng như các cân nhắc thiết kế quan trọng.

Hãy bắt đầu bằng cách xem xét kỹ hơn “Kế thừa trong Java là gì?”

2. Kế thừa trong Java là gì?

Kế thừa Java là một cơ chế trong đó một lớp (lớp cha/lớp cha) truyền các đặc tính và chức năng của nó cho một lớp khác (lớp con/lớp con). Với kế thừa, các trường (biến) và phương thức (hàm) được định nghĩa trong lớp cha có thể được tái sử dụng trong lớp con.

Cơ chế này làm cho việc tổ chức và quản lý mã dễ dàng hơn, tập trung các quy trình chia sẻ, và linh hoạt mở rộng hoặc sửa đổi chức năng. Kế thừa là một trong ba trụ cột cốt lõi của Lập trình Hướng Đối tượng (OOP), cùng với đóng gói và đa hình.

Về Mối quan hệ “is-a”

Một ví dụ phổ biến về kế thừa là mối quan hệ “is-a”. Ví dụ, “một Con chó là một Con vật.” Điều này có nghĩa là lớp Dog kế thừa từ lớp Animal. Một Con chó có thể tiếp nhận các đặc tính và hành vi của Con vật trong khi thêm các tính năng độc đáo của riêng nó.

class Animal {
    void eat() {
        System.out.println("食べる");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("ワンワン");
    }
}

Trong ví dụ này, lớp Dog kế thừa từ lớp Animal. Một thể hiện của Dog có thể sử dụng cả phương thức bark và phương thức eat được kế thừa.

Điều gì xảy ra khi bạn sử dụng Kế thừa?

  • Bạn có thể tập trung logic và dữ liệu chia sẻ trong lớp cha, giảm nhu cầu viết lại cùng mã lặp lại trong mỗi lớp con.
  • Mỗi lớp con có thể thêm hành vi độc đáo của riêng mình hoặc ghi đè các phương thức của lớp cha.

Sử dụng kế thừa giúp tổ chức cấu trúc chương trình và làm cho việc thêm tính năng và bảo trì dễ dàng hơn. Tuy nhiên, kế thừa không phải lúc nào cũng là lựa chọn tốt nhất, và điều quan trọng là phải đánh giá cẩn thận xem có tồn tại mối quan hệ “is-a” thực sự trong quá trình thiết kế.

3. Từ khóa extends hoạt động như thế nào

Từ khóa extends trong Java khai báo rõ ràng kế thừa lớp. Khi một lớp con kế thừa chức năng của lớp cha, cú pháp extends ParentClassName được sử dụng trong khai báo lớp. Điều này cho phép lớp con sử dụng tất cả các thành viên công khai (trường và phương thức) của lớp cha trực tiếp.

Cú pháp Cơ bản

class ParentClass {
    // Fields and methods of the parent class
}

class ChildClass extends ParentClass {
    // Fields and methods unique to the child class
}

Ví dụ, sử dụng các lớp AnimalDog trước đó, chúng ta có:

class Animal {
    void eat() {
        System.out.println("食べる");
    }
}

class Dog extends Animal {
    void bark() {
        System.out.println("ワンワン");
    }
}

Bằng cách viết Dog extends Animal, lớp Dog kế thừa từ lớp Animal và có thể sử dụng phương thức eat.

Sử dụng Thành viên Lớp Cha

Với kế thừa, một thể hiện của lớp con có thể truy cập các phương thức và trường của lớp cha (miễn là bộ điều chỉnh truy cập cho phép):

Dog dog = new Dog();
dog.eat();   // Calls the parent class method
dog.bark();  // Calls the child class method

Ghi Chú Quan Trọng

  • Java chỉ cho phép kế thừa từ một lớp (kế thừa đơn). Bạn không thể chỉ định nhiều lớp sau extends .
  • Nếu bạn muốn ngăn chặn kế thừa, bạn có thể sử dụng modifier final trên lớp.

Mẹo Phát Triển Thực Tế

Sử dụng extends một cách đúng đắn cho phép bạn tập trung chức năng chung vào lớp cha và mở rộng hoặc tùy chỉnh hành vi trong các lớp con. Nó cũng hữu ích khi bạn muốn thêm tính năng mới mà không sửa đổi mã hiện có.

4. Ghi Đè Phương Thức Và Từ Khóa super

Khi sử dụng kế thừa, có những trường hợp bạn muốn thay đổi hành vi của một phương thức được định nghĩa trong lớp cha. Điều này được gọi là “ghi đè phương thức.” Trong Java, ghi đè được thực hiện bằng cách định nghĩa một phương thức trong lớp con có cùng tên và cùng danh sách tham số với phương thức trong lớp cha.

Ghi Đè Phương Thức

Khi ghi đè một phương thức, thường thêm annotation @Override. Điều này giúp trình biên dịch phát hiện các lỗi ngẫu nhiên như tên phương thức sai hoặc chữ ký sai.

class Animal {
    void eat() {
        System.out.println("食べる");
    }
}

class Dog extends Animal {
    @Override
    void eat() {
        System.out.println("ドッグフードを食べる");
    }
}

Trong ví dụ này, lớp Dog ghi đè phương thức eat. Khi gọi eat trên một instance của Dog, đầu ra sẽ là “ドッグフードを食べる”.

Dog dog = new Dog();
dog.eat(); // Displays: ドッグフードを食べる

Sử Dụng Từ Khóa super

Nếu bạn muốn gọi phương thức gốc của lớp cha bên trong phương thức bị ghi đè, sử dụng từ khóa super.

class Dog extends Animal {
    @Override
    void eat() {
        super.eat(); // Calls the parent class’s eat()
        System.out.println("ドッグフードも食べる");
    }
}

Điều này thực thi phương thức eat của lớp cha trước và sau đó thêm hành vi của lớp con.

Constructors Và super

Nếu lớp cha có constructor với tham số, lớp con phải gọi nó một cách rõ ràng bằng super(arguments) làm dòng đầu tiên của constructor của nó.

class Animal {
    Animal(String name) {
        System.out.println("Animal: " + name);
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
        System.out.println("Dog: " + name);
    }
}

Tóm Tắt

  • Ghi đè nghĩa là định nghĩa lại phương thức của lớp cha trong lớp con.
  • Sử dụng annotation @Override được khuyến nghị.
  • Sử dụng super khi bạn muốn tái sử dụng triển khai phương thức của lớp cha.
  • super cũng được sử dụng khi gọi constructors của lớp cha.

5. Ưu Điểm Và Nhược Điểm Của Kế Thừa

Sử dụng kế thừa trong Java mang lại nhiều lợi ích cho thiết kế và phát triển chương trình. Tuy nhiên, sử dụng sai có thể dẫn đến các vấn đề nghiêm trọng. Dưới đây, chúng tôi giải thích chi tiết về ưu điểm và nhược điểm.

Ưu Điểm Của Kế Thừa

  1. Cải thiện khả năng tái sử dụng mã Định nghĩa logic và dữ liệu chung trong lớp cha loại bỏ nhu cầu lặp lại cùng mã trong mỗi lớp con. Điều này giảm trùng lặp và cải thiện khả năng bảo trì và đọc mã.
  2. Dễ dàng mở rộng hơn Khi cần chức năng mới, bạn có thể tạo lớp con mới dựa trên lớp cha mà không sửa đổi mã hiện có. Điều này giảm thiểu tác động thay đổi và giảm cơ hội lỗi.
  3. Cho phép đa hình Kế thừa cho phép “một biến của lớp cha tham chiếu đến instance của lớp con.” Điều này cho phép thiết kế linh hoạt sử dụng giao diện chung và hành vi đa hình.

Nhược Điểm Của Kế Thừa

  1. Các hệ thống phân cấp sâu làm phức tạp thiết kế Nếu chuỗi kế thừa phát triển quá sâu, sẽ khó hiểu hành vi được định nghĩa ở đâu, làm cho việc bảo trì khó khăn hơn.
  2. Thay đổi trong lớp cha ảnh hưởng đến tất cả các lớp con Việc sửa đổi hành vi của lớp cha có thể vô tình gây ra vấn đề trên tất cả các lớp con. Các lớp cha yêu cầu thiết kế và cập nhật cẩn thận.
  3. Có thể giảm tính linh hoạt của thiết kế Việc lạm dụng kế thừa làm các lớp kết nối chặt chẽ, làm cho các thay đổi trong tương lai khó khăn. Trong một số trường hợp, mối quan hệ “has-a” sử dụng composition linh hoạt hơn kế thừa “is-a”.

Tóm tắt

Kế thừa rất mạnh mẽ, nhưng việc dựa vào nó cho mọi thứ có thể gây ra các vấn đề dài hạn. Luôn xác minh xem có mối quan hệ “is-a” thực sự tồn tại không và chỉ áp dụng kế thừa khi phù hợp.

6. Sự khác biệt giữa Kế thừa và Giao diện

Java cung cấp hai cơ chế quan trọng để mở rộng và tổ chức chức năng: kế thừa lớp (extends) và giao diện (implements). Cả hai hỗ trợ tái sử dụng mã và thiết kế linh hoạt, nhưng cấu trúc và cách sử dụng dự định của chúng khác biệt đáng kể. Dưới đây, chúng tôi giải thích sự khác biệt và cách chọn giữa chúng.

Sự khác biệt giữa extendsimplements

  • extends (Kế thừa)
  • Bạn chỉ có thể kế thừa từ một lớp (kế thừa đơn).
  • Các trường và phương thức đã triển khai đầy đủ từ lớp cha có thể được sử dụng trực tiếp trong lớp con.
  • Đại diện cho mối quan hệ “is-a” (ví dụ, một Dog là một Animal).
  • implements (Triển khai Giao diện)
  • Có thể triển khai nhiều giao diện đồng thời.
  • Giao diện chỉ chứa các khai báo phương thức (mặc dù các phương thức mặc định tồn tại từ Java 8 trở đi).
  • Đại diện cho mối quan hệ “can-do” (ví dụ, một Dog có thể sủa, một Dog có thể đi bộ).

Ví dụ về Sử dụng Giao diện

interface Walkable {
    void walk();
}

interface Barkable {
    void bark();
}

class Dog implements Walkable, Barkable {
    public void walk() {
        System.out.println("歩く");
    }
    public void bark() {
        System.out.println("ワンワン");
    }
}

Trong ví dụ này, lớp Dog triển khai hai giao diện, WalkableBarkable, cung cấp hành vi tương tự như kế thừa đa.

Tại sao Giao diện Là Cần Thiết

Java cấm kế thừa đa lớp vì nó có thể tạo ra xung đột khi các lớp cha định nghĩa cùng phương thức hoặc trường. Giao diện giải quyết vấn đề này bằng cách cho phép một lớp áp dụng nhiều “loại” mà không kế thừa các triển khai xung đột.

Cách Sử dụng Chúng Đúng Cách

  • Sử dụng extends khi có mối quan hệ “is-a” rõ ràng giữa các lớp.
  • Sử dụng implements khi bạn muốn cung cấp các hợp đồng hành vi chung trên nhiều lớp.

Ví dụ:

  • “A Dog is an Animal” → Dog extends Animal
  • “A Dog can walk and can bark” → Dog implements Walkable, Barkable

Tóm tắt

  • Một lớp chỉ có thể kế thừa từ một lớp cha, nhưng nó có thể triển khai nhiều giao diện.
  • Việc chọn giữa kế thừa và giao diện dựa trên ý định thiết kế dẫn đến mã sạch, linh hoạt và dễ bảo trì.

7. Các Thực hành Tốt nhất cho Việc Sử dụng Kế thừa

Kế thừa trong Java rất mạnh mẽ, nhưng sử dụng không đúng cách có thể làm cho chương trình cứng nhắc và khó bảo trì. Dưới đây là các thực hành tốt nhất và hướng dẫn để sử dụng kế thừa an toàn và hiệu quả.

Khi Nào Sử dụng Kế thừa — và Khi Nào Tránh Nó

  • Sử dụng kế thừa khi:
  • Có mối quan hệ “is-a” rõ ràng (ví dụ, một Dog là một Animal).
  • Bạn muốn tái sử dụng chức năng lớp cha và mở rộng nó.
  • Bạn muốn loại bỏ mã dư thừa và tập trung logic chung.
  • Tránh kế thừa khi:
  • Bạn chỉ sử dụng nó cho việc tái sử dụng mã (điều này thường dẫn đến thiết kế lớp không tự nhiên).
  • Mối quan hệ “has-a” phù hợp hơn — trong những trường hợp như vậy, hãy xem xét composition.

Chọn Giữa Kế thừa và Composition

  • Kế thừa (extends): quan hệ is-a
  • Ví dụ: Dog extends Animal
  • Hữu ích khi lớp con thực sự đại diện cho một loại của lớp cha.
  • Composition (has-a relationship)
  • Ví dụ: Một chiếc Car có một Engine
  • Sử dụng một thể hiện của lớp khác bên trong để thêm chức năng.
  • Linh hoạt hơn và dễ thích nghi với các thay đổi trong tương lai.

Hướng dẫn thiết kế để ngăn ngừa việc lạm dụng kế thừa

  • Không tạo ra cây kế thừa quá sâu (giữ tối đa 3 cấp hoặc ít hơn).
  • Nếu có nhiều lớp con kế thừa từ cùng một lớp cha, hãy đánh giá lại xem trách nhiệm của lớp cha có phù hợp không.
  • Luôn cân nhắc rủi ro rằng các thay đổi trong lớp cha sẽ ảnh hưởng tới tất cả các lớp con.
  • Trước khi áp dụng kế thừa, hãy xem xét các lựa chọn thay thế như interface và composition.

Hạn chế kế thừa bằng từ khóa final

  • Thêm final vào một lớp sẽ ngăn không cho lớp đó được kế thừa.
  • Thêm final vào một phương thức sẽ ngăn không cho phương thức đó bị ghi đè bởi các lớp con.
    final class Utility {
        // This class cannot be inherited
    }
    
    class Base {
        final void show() {
            System.out.println("オーバーライド禁止");
        }
    }
    

Cải thiện tài liệu và chú thích

  • Ghi chép các quan hệ kế thừa và ý định thiết kế lớp trong Javadoc hoặc trong các chú thích giúp việc bảo trì trong tương lai trở nên dễ dàng hơn rất nhiều.

Tóm tắt

Kế thừa rất tiện lợi, nhưng phải được sử dụng một cách có chủ đích. Luôn tự hỏi, “Liệu lớp này thực sự là một loại của lớp cha không?” Nếu không chắc, hãy cân nhắc sử dụng composition hoặc interface như các lựa chọn thay thế.

8. Tóm tắt

Cho đến thời điểm này, chúng ta đã giải thích chi tiết về kế thừa trong Java và từ khóa extends — từ những nguyên tắc cơ bản đến cách sử dụng thực tế. Dưới đây là bản tóm tắt các điểm chính đã được đề cập trong bài viết.

  • Kế thừa trong Java cho phép một lớp con tiếp nhận dữ liệu và chức năng của lớp cha, giúp thiết kế chương trình hiệu quả và tái sử dụng.
  • Từ khóa extends làm rõ mối quan hệ giữa lớp cha và lớp con (quan hệ “is-a”).
  • Ghi đè phương thức và từ khóa super cho phép mở rộng hoặc tùy chỉnh hành vi được kế thừa.
  • Kế thừa mang lại nhiều lợi thế, như tái sử dụng mã, khả năng mở rộng và hỗ trợ đa hình, nhưng cũng có nhược điểm như cây kế thừa sâu hoặc phức tạp và các thay đổi có ảnh hưởng rộng.
  • Hiểu sự khác nhau giữa kế thừa, interface và composition là yếu tố then chốt để chọn cách tiếp cận thiết kế phù hợp.
  • Tránh lạm dụng kế thừa; luôn rõ ràng về mục đích và lý do thiết kế.

Kế thừa là một trong những khái niệm cốt lõi của lập trình hướng đối tượng trong Java. Khi hiểu rõ các quy tắc và thực hành tốt, bạn sẽ có thể áp dụng nó một cách hiệu quả trong phát triển thực tế.

9. Câu hỏi thường gặp (FAQ)

Q1: Điều gì xảy ra với constructor của lớp cha khi một lớp được kế thừa trong Java?
A1: Nếu lớp cha có constructor không đối số (mặc định), nó sẽ được gọi tự động từ constructor của lớp con. Nếu lớp cha chỉ có constructor có tham số, lớp con phải gọi rõ ràng nó bằng cách sử dụng super(arguments) ở đầu constructor của mình.

Q2: Java có thể thực hiện kế thừa đa lớp không?
A2: Không. Java không hỗ trợ kế thừa đa lớp. Một lớp chỉ có thể extends một lớp cha duy nhất. Tuy nhiên, một lớp có thể implements nhiều interface.

Q3: Sự khác nhau giữa kế thừa và composition là gì?
A3: Kế thừa biểu thị một “quan hệ is-a”, trong đó lớp con tái sử dụng chức năng và dữ liệu của lớp cha. Composition biểu thị một “quan hệ has-a”, trong đó một lớp chứa một thể hiện của lớp khác. Composition thường mang lại tính linh hoạt cao hơn và được ưu tiên trong nhiều trường hợp cần sự gắn kết lỏng lẻo hoặc khả năng mở rộng trong tương lai.

Q4: Modifier final có hạn chế việc kế thừa và ghi đè không?
A4: Có. Nếu một lớp được đánh dấu là final, nó không thể được kế thừa. Nếu một phương thức được đánh dấu là final, nó không thể bị ghi đè trong lớp con. Điều này hữu ích để đảm bảo hành vi nhất quán hoặc vì mục đích bảo mật.

Q5: Điều gì sẽ xảy ra nếu lớp cha và lớp con định nghĩa các trường hoặc phương thức cùng tên?
A5: Nếu một trường cùng tên được định nghĩa trong cả hai lớp, trường trong lớp con sẽ ẩn trường trong lớp cha (shadowing). Các phương thức lại hành xử khác nhau: nếu chữ ký (signature) trùng khớp, phương thức của lớp con sẽ ghi đè phương thức của lớp cha. Lưu ý rằng các trường không thể bị ghi đè—chỉ có thể bị ẩn.

Q6: Điều gì sẽ xảy ra nếu độ sâu kế thừa trở nên quá lớn?
A6: Các cây kế thừa sâu làm cho mã khó hiểu và bảo trì hơn. Việc theo dõi nơi logic được định nghĩa trở nên khó khăn. Để thiết kế có khả năng bảo trì, hãy cố gắng giữ độ sâu kế thừa nông và vai trò được tách biệt rõ ràng.

Q7: Sự khác biệt giữa ghi đè (overriding) và nạp chồng (overloading) là gì?
A7: Ghi đè định nghĩa lại một phương thức từ lớp cha trong lớp con. Nạp chồng định nghĩa nhiều phương thức trong cùng một lớp có cùng tên nhưng khác nhau về kiểu hoặc số lượng tham số.

Q8: Các lớp trừu tượng và giao diện nên được sử dụng khác nhau như thế nào?
A8: Lớp trừu tượng được dùng khi bạn muốn cung cấp triển khai chung hoặc các trường chung cho các lớp liên quan. Giao diện được dùng khi bạn muốn định nghĩa các hợp đồng hành vi mà nhiều lớp có thể thực thi. Hãy sử dụng lớp trừu tượng cho mã chia sẻ và sử dụng giao diện khi đại diện cho nhiều loại hoặc khi cần nhiều “khả năng” khác nhau.