Trong Java, từ khóa extends
là một phần cốt lõi của lập trình hướng đối tượng (OOP), đóng vai trò quan trọng trong việc thiết lập mối quan hệ kế thừa giữa các lớp. Kế thừa cho phép một lớp (gọi là lớp con) kế thừa thuộc tính và phương thức từ một lớp khác (gọi là lớp cha), tạo điều kiện cho việc tái sử dụng và mở rộng mã nguồn mà không cần viết lại từ đầu. Khi sử dụng từ khóa extends
, lớp con có thể thêm hoặc tùy chỉnh các phương thức và thuộc tính của riêng mình, đồng thời vẫn giữ được tính năng của lớp cha.
Sử dụng extends
trong Java không chỉ giúp tối ưu hóa quá trình phát triển phần mềm bằng cách giảm thiểu sự trùng lặp mã nguồn, mà còn tăng cường tính mô-đun, giúp hệ thống dễ dàng bảo trì và mở rộng hơn. Ví dụ, trong một ứng dụng quản lý nhân sự, bạn có thể có một lớp Employee
với các thuộc tính và phương thức cơ bản, và từ đó mở rộng để tạo ra các lớp cụ thể như Manager
và Developer
, mỗi lớp có các đặc điểm và hành vi riêng biệt, nhưng vẫn kế thừa các tính năng cơ bản từ Employee
.
Tầm quan trọng của extends
trong OOP còn được nhấn mạnh qua khả năng hỗ trợ đa hình, cho phép các đối tượng của lớp con được xử lý như thể chúng là các thể hiện của lớp cha, từ đó tăng cường tính linh hoạt và khả năng tái sử dụng trong mã nguồn. Qua đó, extends
không chỉ là một công cụ kỹ thuật, mà còn là một phần của tư duy thiết kế, giúp lập trình viên xây dựng các ứng dụng rõ ràng, tổ chức, và dễ mở rộng.
Kế thừa trong Java
Kế thừa (Inheritance) là một khái niệm cơ bản trong lập trình hướng đối tượng (OOP) mà cho phép một lớp (gọi là lớp con) kế thừa các thuộc tính và phương thức từ một lớp khác (gọi là lớp cha). Kế thừa tạo điều kiện cho việc tái sử dụng mã, giúp giảm sự trùng lặp và tăng tính mô-đun của mã nguồn. Thông qua kế thừa, lớp con có thể mở rộng hoặc tinh chỉnh các chức năng của lớp cha mà không cần phải viết lại toàn bộ mã đã có.
Trong Java, từ khóa extends
được sử dụng để thiết lập mối quan hệ kế thừa giữa các lớp. Khi một lớp được khai báo kế thừa từ một lớp khác, lớp con tự động kế thừa tất cả các thuộc tính và phương thức có thể truy cập của lớp cha, ngoại trừ những phương thức và thuộc tính được đánh dấu là private
. Điều này cho phép lớp con có khả năng sử dụng lại mã nguồn của lớp cha một cách hiệu quả.
class Vehicle { public void display() { System.out.println("Đây là một phương tiện"); } } class Car extends Vehicle { public void showType() { System.out.println("Đây là một chiếc xe hơi"); } } public class TestInheritance { public static void main(String[] args) { Car myCar = new Car(); myCar.display(); // Gọi phương thức từ lớp cha myCar.showType(); // Gọi phương thức từ lớp con } }
Trong ví dụ trên, lớp Car
kế thừa từ lớp Vehicle
sử dụng từ khóa extends
. Điều này cho phép đối tượng myCar
của lớp Car
không chỉ gọi phương thức showType
được định nghĩa trong lớp con mà còn gọi được phương thức display
từ lớp cha Vehicle
.
Kế thừa qua extends
không chỉ giúp tái sử dụng mã và giảm sự trùng lặp, mà còn hỗ trợ đa hình – một trong những nguyên tắc chính của OOP, cho phép các đối tượng của lớp con được xử lý như đối tượng của lớp cha. Điều này mở ra cơ hội lớn trong việc thiết kế và triển khai mã nguồn dễ bảo trì, mở rộng và linh hoạt.
Sử dụng extends để tạo lớp con
Trong Java, từ khóa extends
được sử dụng để tạo một lớp con từ một lớp cha, thiết lập mối quan hệ kế thừa giữa hai lớp. Khi một lớp con kế thừa từ một lớp cha, nó tự động nhận được tất cả các thuộc tính và phương thức (ngoại trừ những phần được đánh dấu là private
) từ lớp cha mà không cần phải định nghĩa lại chúng trong lớp con. Điều này giúp giảm thiểu sự trùng lặp mã nguồn và tăng cường khả năng tái sử dụng mã.
Cách sử dụng extends
:
Để sử dụng extends
, bạn chỉ cần khai báo lớp con kèm theo từ khóa extends
sau đó là tên của lớp cha mà bạn muốn lớp con kế thừa.
Ví dụ minh họa:
Giả sử chúng ta có một lớp cha Animal
với một số thuộc tính và phương thức cơ bản:
class Animal { String name; public void eat() { System.out.println(name + " đang ăn."); } }
Bây giờ, chúng ta muốn tạo một lớp con Dog
kế thừa từ lớp Animal
:
class Dog extends Animal { public void bark() { System.out.println(name + " đang sủa."); } }
Trong ví dụ trên, lớp Dog
kế thừa lớp Animal
sử dụng từ khóa extends
. Điều này có nghĩa là Dog
tự động nhận được thuộc tính name
và phương thức eat()
từ lớp Animal
. Ngoài ra, lớp Dog
cũng định nghĩa thêm một phương thức riêng của mình là bark()
.
public class TestInheritance { public static void main(String[] args) { Dog myDog = new Dog(); myDog.name = "Rex"; myDog.eat(); // Gọi phương thức từ lớp cha myDog.bark(); // Gọi phương thức từ lớp con } }
Khi chạy đoạn mã trên, đối tượng myDog
có thể gọi cả phương thức eat()
được kế thừa từ lớp cha Animal
và phương thức bark()
được định nghĩa trong chính lớp Dog
.
Qua đó, extends
giúp tạo lớp con Dog
mà không chỉ kế thừa các thuộc tính và phương thức từ lớp cha Animal
mà còn có thể mở rộng với các đặc điểm và hành vi mới, minh họa tính linh hoạt và mạnh mẽ của kế thừa trong Java.
Ghi đè phương thức
Trong Java, khả năng một lớp con ghi đè phương thức của lớp cha là một phần quan trọng của kế thừa và đa hình, cho phép các lớp con cung cấp triển khai cụ thể cho một phương thức đã được định nghĩa trong lớp cha. Khi một phương thức trong lớp con có cùng chữ ký với một phương thức trong lớp cha, phương thức đó được coi là đã ghi đè lên phương thức của lớp cha.
Ghi đè phương thức cho phép lớp con tùy chỉnh hoặc mở rộng hành vi của lớp cha. Điều này đặc biệt hữu ích trong việc thiết kế các lớp có hành vi mặc định từ lớp cha nhưng cần được tinh chỉnh hoặc thay đổi hoàn toàn trong các lớp con.
Ví dụ minh họa:
Xét một lớp cha Animal
với phương thức makeSound()
:
class Animal { public void makeSound() { System.out.println("Một số tiếng động"); } }
Và một lớp con Dog
ghi đè phương thức makeSound()
:
class Dog extends Animal { @Override public void makeSound() { System.out.println("Woof"); } }
Trong ví dụ trên, từ khóa @Override
được sử dụng trước phương thức makeSound()
trong lớp Dog
để chỉ ra rằng lớp Dog
đang ghi đè phương thức makeSound()
từ lớp Animal
. Khi một đối tượng của lớp Dog
gọi phương thức makeSound()
, Java sẽ thực thi phương thức makeSound()
được định nghĩa trong lớp Dog
, không phải phương thức từ lớp Animal
.
public class TestOverride { public static void main(String[] args) { Animal myDog = new Dog(); myDog.makeSound(); // In ra "Woof" } }
Trong đoạn mã trên, mặc dù myDog
được khai báo với kiểu Animal
, phương thức makeSound()
của lớp Dog
được gọi do tính đa hình và việc ghi đè phương thức.
Ghi đè phương thức là một công cụ mạnh mẽ cho phép lập trình viên xây dựng các hệ thống phần mềm linh hoạt và dễ mở rộng, bằng cách cung cấp khả năng tùy chỉnh hành vi của lớp cha trong các lớp con.
Sử dụng từ khóa super
Trong Java, từ khóa super
được sử dụng trong một lớp con để truy cập trực tiếp tới thuộc tính, phương thức và constructor của lớp cha. Điều này rất hữu ích trong các tình huống mà lớp con muốn mở rộng hoặc tùy chỉnh chức năng của lớp cha, thay vì thay thế hoàn toàn.
Sử dụng super
giúp duy trì một mối quan hệ rõ ràng giữa lớp con và lớp cha, và cung cấp một cách để lớp con tương tác với lớp cha một cách minh bạch.
Truy cập thuộc tính của lớp cha:
Khi một lớp con có một thuộc tính cùng tên với một thuộc tính trong lớp cha, từ khóa super
có thể được sử dụng để phân biệt thuộc tính của lớp cha.
Truy cập phương thức của lớp cha:
Lớp con có thể gọi phương thức của lớp cha mà đã được ghi đè bằng cách sử dụng super
.
Gọi constructor của lớp cha:
super
cũng được sử dụng trong constructor của lớp con để gọi constructor của lớp cha.
Ví dụ về việc sử dụng super
trong lớp con:
Giả sử chúng ta có lớp cha Vehicle
và lớp con Car
như sau:
class Vehicle { String type = "Vehicle"; void display() { System.out.println("This is a " + type); } } class Car extends Vehicle { String type = "Car"; void display() { super.display(); // Gọi phương thức display của lớp cha System.out.println("This is a " + type); } } public class TestSuper { public static void main(String[] args) { Car myCar = new Car(); myCar.display(); } }
Trong ví dụ này, phương thức display
trong lớp Car
gọi phương thức display
của lớp cha bằng cách sử dụng super.display()
. Khi phương thức display
được gọi từ đối tượng myCar
, nó sẽ đầu tiên in ra thông điệp từ phương thức của lớp cha (“This is a Vehicle”) do gọi super.display()
, sau đó in ra thông điệp từ lớp con (“This is a Car”).
Qua đó, super
cung cấp một cách linh hoạt và mạnh mẽ để lớp con tương tác với lớp cha, cho phép lớp con tận dụng và mở rộng chức năng của lớp cha theo cách minh bạch và hiệu quả.
Hạn chế của kế thừa
Kế thừa là một khái niệm quan trọng trong lập trình hướng đối tượng, nhưng nó không phải lúc nào cũng là giải pháp tốt nhất cho mọi vấn đề. Một số hạn chế của kế thừa bao gồm “Vấn đề lớp cơ sở mong manh” (Fragile Base Class Problem) và giới hạn của kế thừa đơn.
Vấn đề lớp cơ sở mong manh (Fragile Base Class Problem):
Vấn đề này xảy ra khi một thay đổi trong lớp cơ sở (lớp cha) có thể không ngờ tới mà ảnh hưởng đến hành vi của một hoặc nhiều lớp con kế thừa từ nó. Do tính chất kết nối chặt chẽ của kế thừa, thậm chí những thay đổi nhỏ trong lớp cơ sở cũng có thể gây ra hậu quả không mong muốn hoặc lỗi trong các lớp con, khiến hệ thống trở nên mong manh và khó bảo trì.
Giới hạn của kế thừa đơn:
Trong nhiều ngôn ngữ lập trình bao gồm Java, một lớp chỉ có thể kế thừa trực tiếp từ một lớp cha duy nhất. Điều này giới hạn khả năng tái sử dụng mã nguồn vì một lớp con không thể kế thừa hành vi từ nhiều lớp cha. Trong một số trường hợp, kế thừa đơn không thể biểu diễn một cách đầy đủ mối quan hệ phức tạp giữa các đối tượng.
Khi nào nên sử dụng kế thừa:
Kế thừa nên được sử dụng khi có một mối quan hệ tự nhiên “là một” (is-a) giữa lớp con và lớp cha, và bạn muốn lớp con kế thừa thuộc tính và hành vi từ lớp cha. Kế thừa làm tăng tính mô-đun và tái sử dụng mã nguồn, đặc biệt hữu ích trong việc xây dựng các hệ thống phân cấp.
Khi nào nên xem xét các phương pháp khác như composition:
Khi mối quan hệ giữa các đối tượng không rõ ràng là mối quan hệ “là một”, hoặc khi bạn muốn tránh sự phụ thuộc chặt chẽ của kế thừa, bạn nên xem xét sử dụng composition. Composition (kết hợp) cho phép một đối tượng chứa tham chiếu đến đối tượng khác (“có một” hoặc “sử dụng một” mối quan hệ) và sử dụng chức năng của nó, đem lại sự linh hoạt cao hơn và giảm sự phụ thuộc giữa các lớp.
Ví dụ, thay vì kế thừa từ lớp Car
, một lớp ElectricCar
có thể chứa một tham chiếu đến một đối tượng Battery
và sử dụng các dịch vụ của nó, thể hiện mối quan hệ composition.
Tóm lại, trong khi kế thừa là một công cụ mạnh mẽ trong OOP, nó cũng có những hạn chế và không phải lúc n
ào cũng là lựa chọn tốt nhất. Lập trình viên nên cân nhắc cẩn thận khi chọn giữa kế thừa và composition dựa trên yêu cầu cụ thể của hệ thống và mối quan hệ giữa các đối tượng.
Một số ví dụ về extend trong java
- Kế thừa từ class cha:
class Animal { protected String name; protected int age; public void eat() { System.out.println("Eating..."); } } class Dog extends Animal { public void bark() { System.out.println("Barking..."); } } class Cat extends Animal { public void meow() { System.out.println("Meowing..."); } }
- Kế thừa từ nhiều lớp:
interface Drivable { void drive(); } interface Flyable { void fly(); } class Airplane extends Object implements Drivable, Flyable { public void drive() { System.out.println("Driving an airplane"); } public void fly() { System.out.println("Flying an airplane"); } }
- Kế thừa từ class Abstract:
abstract class Shape { abstract void draw(); } class Circle extends Shape { void draw() { System.out.println("Drawing Circle"); } } class Square extends Shape { void draw() { System.out.println("Drawing Square"); } }
Trong các ví dụ trên chỉ là một số trong nhiều cách sử dụng extends trong Java, có thể có nhiều cách khác để sử dụng extends tùy theo nhu cầu của chương trình và mục đích sử dụng. Sử dụng extends giúp cho việc code dễ quản lý và dễ bảo trì hơn, giúp tăng tính tái sử dụng mã nguồn, giảm sự phụ thuộc giữa các lớp và giúp cho chương trình dễ dàng hơn để mở rộng và bảo trì.