Tính Kế thừa (Inheritance) và tính đa hình (Polymorphism) trong hướng đối tượng
- 03-10-2023
- Toanngo92
- 0 Comments
Xét một tình huống trong đó một học sinh đang phát triển một trang web trực tuyến hiển thị thói quen ăn uống của các loài động vật khác nhau trên trái đất. Trong quá trình thiết kế và phát triển trang web thói quen ăn uống trực tuyến, học sinh nhận ra rằng có nhiều loài động vật và chim có thể ăn cùng loại thức ăn và có các đặc điểm tương tự. Các nhóm con của tất cả các loài động vật ăn thực phẩm từ thực vật được gọi là thực vật ăn, những con ăn thịt được gọi là ăn thịt và những con ăn cả thực vật và động vật được gọi là ăn tạp. Loại hình phân loại này của các thứ gọi là phân lớp con và các nhóm con được gọi là phân lớp con (sub class). Tương tự, Java cung cấp khái niệm về kế thừa để tạo các phân lớp con của một lớp cụ thể.
Ngoài ra, có các động vật như thằn lằn có thể thay đổi màu sắc của họ dựa trên môi trường. Con người cũng đóng các vai trò khác nhau trong cuộc sống hàng ngày của họ như cha, con trai, chồng, và vân vân. Điều này có nghĩa rằng họ thể hiện sự khác biệt trong các tình huống khác nhau. Tương tự, Java cung cấp tính năng gọi là đa hình trong đó các đối tượng thể hiện hành vi khác nhau dựa trên ngữ cảnh mà chúng được sử dụng.
Mục lục
Tính kế thừa
Trong cuộc sống hàng ngày, người ta thường gặp các đối tượng có mối quan hệ với nhau. Ví dụ, Xe ô tô là một loại xe bốn bánh, một loại xe bốn bánh là một loại phương tiện, và một loại phương tiện là một loại máy móc. Tương tự, nhiều đối tượng khác có thể được xác định có mối quan hệ như vậy. Tất cả những đối tượng như vậy có các thuộc tính chung. Ví dụ, tất cả các xe bốn bánh đều có cần gạt nước và gương chiếu hậu. Tất cả các phương tiện đều có một biển số xe, bánh xe và động cơ, bất kể là xe bốn bánh hay xe hai bánh.
Hình ảnh mô phỏng
Hình trên thể hiện mối quan hệ là-một giữa các đối tượng khác nhau. Ví dụ, Hươu là một loài ăn cỏ và một loài ăn cỏ là một loài động vật. Các thuộc tính chung của tất cả các loài ăn thực vật có thể được lưu trữ trong lớp herbivore. Tương tự, các thuộc tính chung của tất cả các loại động vật như ăn cỏ, ăn thịt và ăn tạp có thể được lưu trữ trong lớp Animal.
Do đó, lớp Animal trở thành lớp cấp cao nhất từ đó các lớp khác như Herbivore, Carnivore và Omnivore kế thừa các thuộc tính và hành vi. Các lớp Deer, Horse, Lion và những lớp khác kế thừa các thuộc tính từ các lớp Herbivore, Carnivore và vân vân.
Điều này được gọi là kế thừa. Vì vậy, kế thừa trong Java là một tính năng thông qua đó các lớp có thể được tạo ra từ các lớp khác và kế thừa các trường và phương thức từ những lớp đó.
Các tính năng và thuật ngữ
Trong Java, khi thực hiện kế thừa, lớp được tạo ra từ một lớp khác được gọi là lớp kế thừa, lớp con, hoặc lớp mở rộng. Lớp từ đó lớp con được tạo ra được gọi là lớp super (super class), lớp cơ sở (base class), hoặc lớp cha (parent class).
Khái niệm kế thừa đơn giản nhưng mạnh mẽ vì nó cho phép tạo ra một lớp mới từ một lớp hiện có đã có một số mã code cần thiết. Lớp mới được tạo ra từ lớp hiện có có thể sử dụng lại các trường và phương thức của lớp hiện có mà không cần viết lại hoặc sửa lỗi mã code lại.
Một lớp con kế thừa tất cả các thành viên như trường, các lớp lồng nhau và phương thức từ lớp siêu của nó, trừ những thành viên có bổ từ truy cập private. Tuy nhiên, các hàm khởi tạo của một lớp không được xem xét là thành viên của lớp và không được kế thừa bởi lớp con. Lớp con có thể gọi hàm khởi tạo của lớp super từ hàm khởi tạo của nó.
Các thành viên có khả năng truy cập mặc định trong lớp super không được kế thừa bởi lớp con ở các gói khác. Những thành viên này chỉ có thể được truy cập bởi lớp con trong cùng gói với lớp super. Lớp con sẽ có những đặc điểm riêng cũng như những đặc điểm kế thừa từ lớp super.
Hình ảnh mô phỏng kế thừa trong lớp:
Các loại kế thừa khác nhau trong Java bao gồm:
- Kế thừa Đơn (single inheritance): Khi 2 lớp con kế thừa từ một và chỉ một lớp cha, được gọi là kế thừa đơn. Trong hình 7.2, lớp B kế thừa từ lớp A duy nhất.
- Kế thừa Đa Cấp (Multi level inheritance): Khi một lớp con được kế thừa từ một lớp cha mà chính nó là một lớp con của một lớp khác, thì được gọi là kế thừa đa cấp. Trong hình trên, lớp C được kế thừa từ lớp B, lớp B lại được kế thừa từ lớp A.
- Kế thừa Phân Cấp (Hierarchical): Khi một lớp cha có nhiều lớp con ở các cấp độ khác nhau, thì được gọi là kế thừa phân cấp. Trong hình trên, lớp C được kế thừa từ lớp B và các lớp B và D đều được kế thừa từ lớp A.
- Kế thừa Đa Nguồn (Multiple inheritance): Khi một lớp con kế thừa từ hơn một lớp cha, thì được gọi là kế thừa đa nguồn. Java không hỗ trợ kế thừa đa nguồn. Điều này có nghĩa là một lớp trong Java không thể kế thừa từ nhiều hơn một lớp cha. Tuy nhiên, Java cung cấp một cách giải quyết cho tính năng này dưới dạng các giao diện (interfaces). Sử dụng giao diện, người dùng có thể mô phỏng kế thừa bằng cách thực hiện nhiều hơn một giao diện trong một lớp.
Làm việc với super class và sub class
Trong một lớp con, người ta có thể sử dụng các thành viên được kế thừa theo các cách sau:
- Các thành viên được kế thừa, bao gồm trường và phương thức, có thể được sử dụng trực tiếp giống như bất kỳ thành viên nào khác.
- Người ta có thể khai báo một trường có cùng tên trong lớp con như trong lớp cha. Điều này sẽ dẫn đến việc ẩn đi trường của lớp cha, điều này không được khuyến khích.
- Người ta có thể khai báo các trường mới trong lớp con mà không có trong lớp cha. Các thành viên này sẽ chỉ thuộc về lớp con.
- Người ta có thể viết một phương thức instance mới có cùng chữ ký trong lớp con như trong lớp cha. Điều này được gọi là việc ghi đè phương thức.
- Một phương thức tĩnh mới có thể được tạo ra trong lớp con với cùng chữ ký như trong lớp cha. Điều này sẽ dẫn đến việc ẩn đi phương thức của lớp cha.
- Người ta có thể khai báo các phương thức mới trong lớp con mà không có trong lớp cha.
- Một constructor của lớp con có thể được sử dụng để gọi constructor của lớp cha, có thể được thực hiện một cách tự động hoặc bằng cách sử dụng từ khóa “super”.
Từ khóa “extends” được sử dụng để tạo ra một lớp con. Một lớp chỉ có thể được kế thừa trực tiếp từ một lớp. Nếu một lớp không có lớp cha nào, nó sẽ được kế thừa ngầm định từ lớp object.
Cú pháp để tạo một lớp con như sau:
modifier class <sub_class_name> extends <parent_class_name>
Trong đó:
- modifier: bổ từ truy cập
- sub_class_name: Xác định tên của lớp con.
- parent_class_name: Xác định tên của lớp cha.
Ví dụ:
package session;
public class Vehicle {
// Declare common attributes of a vehicle
protected String vehicleNo; // Variable to store vehicle number
protected String vehicleName; // Variable to store vehicle name
protected int wheels; // Variable to store the number of wheels
/**
* Accelerates the vehicle.
*
* @param speed the speed at which the vehicle accelerates
*/
public void accelerate(int speed) {
System.out.println("Accelerating at: " + speed + " kmph");
}
}
package sessions;
class Vehicle {
protected String vehicleNo;
protected String vehicleName;
protected int wheels;
public void accelerate(int speed) {
System.out.println("Accelerating at: " + speed + " kmph");
}
}
class Fourwheeler extends Vehicle {
private boolean powerSteer;
public Fourwheeler(String vid, String vName, int numWheels, boolean pSteer) {
vehicleNo = vid;
vehicleName = vName;
wheels = numWheels;
powerSteer = pSteer;
}
public void showDetails() {
System.out.println("Vehicle No: " + vehicleNo);
System.out.println("Vehicle Name: " + vehicleName);
System.out.println("Number of Wheels: " + wheels);
if (powerSteer) {
System.out.println("Power Steering: Yes");
} else {
System.out.println("Power Steering: No");
}
}
}
public class TestVehicle {
public static void main(String[] args) {
// Create an object of the child class and specify the values
Fourwheeler objFour = new Fourwheeler("LA-09CS-1406", "Volkswagen", 4, true);
objFour.showDetails(); // Invoke child class method
objFour.accelerate(200); // Invoke inherited method
}
}
Lớp Vehicle được định nghĩa ở đoạn mã trên bao gồm các thuộc tính chung của một phương tiện như vehicleNo, vehicleName và wheels. Ngoài ra, nó bao gồm một hành vi chung của một phương tiện, đó là phương thức accelerate() in ra tốc độ mà phương tiện đang tăng tốc.
Đoạn mã dưới lưu ở thành file TestVehicle.java. Đoạn mã mô tả lớp con FourWheeler với thuộc tính riêng của nó là powerSteer. Giá trị cho các thuộc tính được kế thừa và thuộc tính riêng của nó được xác định trong constructor. Phương thức showDetails() được sử dụng để hiển thị tất cả các chi tiết.
Lớp TestVehicle bao gồm phương thức main(). Trong phương thức main(), đối tượng objFour của lớp con được tạo ra và constructor có tham số được gọi bằng cách truyền các đối số thích hợp. Tiếp theo, phương thức showDetails() được gọi để in ra các chi tiết. Ngoài ra, đối tượng của lớp con được sử dụng để gọi phương thức accelerate() được kế thừa từ lớp cha và giá trị của tốc độ được truyền làm đối số.
Output:
Đoạn mã dưới chỉ rõ rằng bạn có thể truy cập các thành viên được bảo vệ (protected) và công khai (public) của một lớp cha trực tiếp trong lớp con do tính kế thừa.
Ghi đè phương thức (Overriding method)
Java cho phép tạo một phương thức thể hiện trong một lớp con có cùng chữ ký và kiểu trả về với một phương thức thể hiện của lớp cha. Điều này được gọi là ghi đè phương thức. Ghi đè phương thức cho phép một lớp kế thừa hành vi từ một lớp cha và sau đó, sửa đổi hành vi theo yêu cầu.
Các quy tắc cần ghi nhớ khi ghi đè phương thức:
- Tên phương thức, số lượng tham số và kiểu trả về phải giống nhau giữa phương thức trong lớp con và phương thức trong lớp cha.
- Phương thức ghi đè trong lớp con không thể có mức độ truy cập thấp hơn (lớn hơn) so với phương thức gốc trong lớp cha. Nó có thể có mức độ truy cập cao hơn (nhỏ hơn) hoặc giống nhau.
- Phương thức ghi đè trong lớp con không thể ném ra các ngoại lệ (exceptions) mới hoặc có phạm vi ngoại lệ rộng hơn so với phương thức gốc trong lớp cha.
- Một phương thức ghi đè không thể sử dụng từ khóa final, static hoặc private, vì các từ khóa này ngăn chặn sự kế thừa và ghi đè.
- Đối tượng của lớp con có thể được gán cho một biến tham chiếu của lớp cha. Khi gọi phương thức, phương thức ghi đè trong lớp con sẽ được gọi nếu phương thức có cùng tên và chữ ký trong lớp cha.
- Để ghi đè phương thức, bạn sử dụng từ khóa @Override trước khai báo phương thức trong lớp con, để làm cho mã dễ đọc hơn và giúp kiểm tra lỗi ghi đè.
Ví dụ:
package sessions;
class Vehicle {
protected String vehicleNo;
protected String vehicleName;
protected int wheels;
public void accelerate(int speed) {
System.out.println("Maximum acceleration: " + speed + " kmph");
}
}
class Fourwheeler extends Vehicle {
private boolean powerSteer;
public Fourwheeler(String vid, String vName, int numWheels, boolean pSteer) {
vehicleNo = vid;
vehicleName = vName;
wheels = numWheels;
powerSteer = pSteer;
}
public void showDetails() {
System.out.println("Vehicle No: " + vehicleNo);
System.out.println("Vehicle Name: " + vehicleName);
System.out.println("Number of Wheels: " + wheels);
if (powerSteer) {
System.out.println("Power Steering: Yes");
} else {
System.out.println("Power Steering: No");
}
}
@Override
public void accelerate(int speed) {
System.out.println("Accelerating at: " + speed + " kmph");
}
}
public class TestVehicle {
public static void main(String[] args) {
// Create an object of the child class and specify the values
Fourwheeler objFour = new Fourwheeler("LA-09CS-1406", "Volkswagen", 4, true);
objFour.showDetails(); // Invoke child class method
objFour.accelerate(200); // Invoke overridden method
}
}
Phương thức accelerate() được ghi đè trong lớp con với cùng chữ ký và kiểu trả về, nhưng có một thông báo đã được sửa đổi. Hãy chú ý việc sử dụng @Override ở đầu của phương thức. Đây là một chú thích (annotation) thông báo cho trình biên dịch rằng phương thức sau đó đã được ghi đè từ lớp cha. Nếu trình biên dịch phát hiện rằng một phương thức như vậy không tồn tại trong lớp cha, nó sẽ tạo ra lỗi biên dịch.
Lưu ý: chú thích (Annotations) cung cấp thông tin bổ sung về một chương trình. Chú thích không có tác động trực tiếp đối với hoạt động của mã mà chú thích đó chú thích.
Output:
Lưu ý rằng phương thức accelerate() hiện tại in ra thông báo được chỉ định trong lớp con. Điều này có nghĩa là khi gọi phương thức accelerate() bằng cách sử dụng đối tượng của lớp con objFour.accelerate(), trước tiên nó tìm kiếm phương thức trong cùng một lớp. Vì phương thức accelerate() đã được ghi đè trong lớp con, nó gọi phiên bản của phương thức accelerate() trong lớp con và không gọi phiên bản của phương thức accelerate() trong lớp cha.
Truy cập hàm khởi tạo (constructor) và Phương thức của Lớp Cha
Trong đoạn mã dưới, constructor của lớp con được sử dụng để khởi tạo giá trị của các thuộc tính chung được kế thừa từ lớp cha Vehicle. Tuy nhiên, đây không phải là cách tiếp cận đúng vì tất cả các lớp con của lớp Vehicle sẽ phải khởi tạo giá trị của các thuộc tính chung mỗi lần trong constructor của chúng.
Sự trùng lặp này không chỉ không hiệu quả mà còn gợi ý rằng để sử dụng những thành viên này, một lớp con. Trong trường hợp cụ thể, người dùng có thể muốn giữ các thành viên dữ liệu có trong một lớp cha ở chế độ riêng tư (private). Trong trường hợp như vậy, lớp con sẽ không thể truy cập các thành viên này trực tiếp và cũng không thể khởi tạo các biến này một cách độc lập. Java cung cấp một giải pháp bằng cách cho phép lớp con gọi constructor và phương thức của lớp cha bằng từ khóa super. Ví dụ: super – member – name, trong đó member có thể là một phương thức hoặc một biến thể hiện. Việc sử dụng từ khóa super rất hữu ích khi tên của thành viên trong lớp con ẩn các thành viên có cùng tên trong lớp cha.
Đoạn mã định nghĩa lớp Vehicle với các thuộc tính vehicleNo, vehicleName, wheels và hàm khởi tạo với các thuộc tính đưược tham số hóa:
package sessions;
public class Vehicle {
protected String vehicleNo; // Variable to store vehicle number
protected String vehicleName; // Variable to store vehicle name
protected int wheels; // Variable to store the number of wheels
/**
* Parameterized constructor to initialize values based on user input.
*
* @param vid a String variable storing vehicle ID
* @param vName a String variable storing vehicle name
* @param numWheels an integer variable storing the number of wheels
*/
public Vehicle(String vid, String vName, int numWheels) {
vehicleNo = vid;
vehicleName = vName;
wheels = numWheels;
}
/**
* Accelerates the vehicle.
*
* @param speed the speed at which the vehicle accelerates
*/
public void accelerate(int speed) {
System.out.println("Accelerating at: " + speed + " kmph");
}
}
Đoạn mã dưới mô tả lớp con FourWheeler đã được sửa đổi, sử dụng từ khóa super để gọi constructor và các phương thức của lớp cha:
package sessions;
class Fourwheeler extends Vehicle {
private boolean powerSteer; // Variable to store steering information
/**
* Parameterized constructor to initialize values based on user input.
*
* @param vid a String variable storing vehicle ID
* @param vName a String variable storing vehicle name
* @param numWheels an integer variable storing number of wheels
* @param pSteer a boolean variable storing steering information
*/
public Fourwheeler(String vid, String vName, int numWheels, boolean pSteer) {
super(vid, vName, numWheels); // Invoke the superclass constructor
powerSteer = pSteer;
}
/**
* Displays vehicle details.
*/
public void showDetails() {
System.out.println("Vehicle No: " + vehicleNo);
System.out.println("Vehicle Name: " + vehicleName);
System.out.println("Number of Wheels: " + wheels);
if (powerSteer) {
System.out.println("Power Steering: Yes");
} else {
System.out.println("Power Steering: No");
}
}
/**
* Overridden method to display acceleration details of the vehicle.
*/
@Override
public void accelerate(int speed) {
super.accelerate(speed); // Invoke the superclass accelerate() method
System.out.println("Maximum acceleration: " + speed + " kmph");
}
}
public class TestVehicle {
public static void main(String[] args) {
// Create an object of the child class and specify the values
Fourwheeler objFour = new Fourwheeler("LA-09CS-1406", "Volkswagen", 4, true);
objFour.showDetails();
objFour.accelerate(200);
}
}
Đoạn mã trên mô tả việc sử dụng super() để gọi constructor của lớp cha từ constructor của lớp con. Tương tự, câu lệnh super.accelerate() được sử dụng để gọi phương thức accelerate() của lớp cha từ lớp con.
Output:
Lưu ý rằng đầu ra hiển thị các câu lệnh của cả hai phương thức accelerate(), tức là của lớp cha và lớp con. Ngoài ra, các đối số cho các thành viên được kế thừa từ lớp cha đã được truyền vào constructor của lớp con. Tuy nhiên, các giá trị đã được truyền vào constructor của lớp cha sử dụng super(), do đó các thành viên của lớp cha đã được khởi tạo trong constructor của lớp cha. Bây giờ, mọi lớp con của lớp Vehicle không cần viết mã để khởi tạo các thuộc tính chung của một phương tiện. Thay vào đó, nó có thể truyền các giá trị vào constructor của lớp cha bằng cách sử dụng super().
Tính đa hình
Từ “polymorph” kết hợp từ hai từ là “poly” có nghĩa là “nhiều” và “morph” có nghĩa là “hình dạng”. Do đó, “polymorph” đề cập đến một đối tượng có thể có nhiều hình dạng khác nhau. Nguyên tắc này cũng có thể được áp dụng cho các lớp con của một lớp có thể xác định các hành vi cụ thể riêng của họ cũng như kế thừa một số chức năng tương tự của lớp cha. Khái niệm ghi đè phương thức là một ví dụ về đa hình trong lập trình hướng đối tượng, trong đó cùng một phương thức hoạt động theo cách khác nhau trong lớp cha và trong lớp con.
Hiểu về Ràng buộc Tĩnh (static binding) và ràng buộc động (dynamic binding)
Khi trình biên dịch giải quyết việc ràng buộc các phương thức và cuộc gọi phương thức tại thời gian biên dịch, nó được gọi là ràng buộc tĩnh hoặc ràng buộc sớm. Nếu trình biên dịch giải quyết các cuộc gọi phương thức và ràng buộc tại thời gian chạy, nó được gọi là ràng buộc động hoặc ràng buộc muộn. Tất cả các cuộc gọi phương thức tĩnh được giải quyết tại thời gian biên dịch và do đó, ràng buộc tĩnh được thực hiện cho tất cả các cuộc gọi phương thức tĩnh. Cuộc gọi phương thức thể hiện luôn được giải quyết vào thời gian chạy.
Các phương thức tĩnh là phương thức của lớp và được truy cập bằng cách sử dụng tên lớp. Việc sử dụng phương thức tĩnh được khuyến nghị vì không cần tham chiếu đối tượng để truy cập chúng và do đó, cuộc gọi phương thức tĩnh được giải quyết trong thời gian biên dịch. Điều này cũng là lý do tại sao các phương thức tĩnh không bị ghi đè.
Tương tự, Java không cho phép hành vi đa hình của các biến của một lớp. Do đó, việc truy cập tất cả các biến cũng tuân theo ràng buộc tĩnh.
Một số khác biệt quan trọng giữa ràng buộc tĩnh và động được liệt kê trong bảng:
Ràng buộc tĩnh | Ràng buộc động |
Ràng buộc tĩnh xảy ra tại thời điểm biên dịch | Ràng buộc động xảy ra tại thời điểm chạy |
Các phương thức và biến private, static và final sử dụng ràng buộc tĩnh và được ràng buộc bởi trình biên dịch. | Các phương thức ảo được ràng buộc tại thời điểm chạy dựa trên đối tượng thời gian chạy. |
Ràng buộc tĩnh sử dụng thông tin về kiểu đối tượng cho việc ràng buộc là kiểu của lớp | Ràng buộc động sử dụng kiểu tham chiếu để giải quyết. |
Các phương thức nạp chồng (overloaded methods) được ràng buộc bằng cách sử dụng ràng buộc tĩnh (static binding) | Các phương thức ghi đè (overridden methods) được ràng buộc bằng cách sử dụng ràng buộc động (dynamic binding). |
Ví dụ mô tả ràng buộc tĩnh:
class Employee {
String empId; // Variable to store employee ID
String empName; // Variable to store employee name
int salary; // Variable to store salary
float commission; // Variable to store commission
/**
* Parameterized constructor to initialize the variables.
*
* @param id a String variable storing employee ID
* @param name a String variable storing employee name
* @param sal an integer variable storing salary
*/
public Employee(String id, String name, int sal) {
empId = id;
empName = name;
salary = sal;
}
/**
* Calculates commission based on sales value.
*
* @param sales a float variable storing sales value
*/
public void calcCommission(float sales) {
if (sales > 10000) {
commission = salary * 20 / 100;
} else {
commission = 0;
}
}
/**
* Overloaded method. Calculates commission based on overtime.
*
* @param overtime an integer variable storing overtime hours
*/
public void calcCommission(int overtime) {
if (overtime > 8) {
commission = salary / 30;
} else {
commission = 0;
}
}
/**
* Displays employee details.
*/
public void displayDetails() {
System.out.println("Employee ID: " + empId);
System.out.println("Employee Name: " + empName);
System.out.println("Salary: " + salary);
System.out.println("Commission: " + commission);
}
}
public class EmployeeDetails {
public static void main(String[] args) {
// Instantiate the Employee class object
Employee objEmp = new Employee("E001", "Maria Nemeth", 40000);
// Invoke the calcCommission() with float argument
objEmp.calcCommission(20000F);
objEmp.displayDetails(); // Print the employee details
}
}
Đoạn mã trên mô tả một lớp Employee với bốn biến thành viên. Constructor được sử dụng để khởi tạo các biến thành viên này với các giá trị nhận được. Lớp này chứa hai phương thức calcCommission(): một để tính hoa hồng dựa trên doanh số bán hàng của nhân viên và một phương thức khác dựa trên số giờ làm thêm của nhân viên. Phương thức displayDetails() được sử dụng để in ra thông tin chi tiết của nhân viên.
Lớp EmployeeDetails khác được tạo ra với phương thức main(). Bên trong phương thức main(), một đối tượng có tên objEmp thuộc lớp Employee được tạo ra, và constructor với tham số được gọi với các đối số thích hợp. Tiếp theo, phương thức calcCommission() được gọi với đối số là 20000F.
Trong ví dụ này, khi phương thức calcCommission() được thực thi, phương thức với đối số kiểu float được gọi vì nó đã được xác định ở thời điểm biên dịch dựa trên kiểu biến, nghĩa là float. Cuối cùng, phương thức displayDetails() được gọi để in ra chi tiết của nhân viên.
Output:
Ví dụ mô tả ràng buộc động:
Xét tình huống lớp PartTimeEmployee kế thừa lớp Employee
class Employee {
String empId; // Variable to store employee ID
String empName; // Variable to store employee name
int salary; // Variable to store salary
float commission; // Variable to store commission
/**
* Parameterized constructor to initialize the variables.
*
* @param id a String variable storing employee ID
* @param name a String variable storing employee name
* @param sal an integer variable storing salary
*/
public Employee(String id, String name, int sal) {
empId = id;
empName = name;
salary = sal;
}
/**
* Calculates commission based on sales value.
*
* @param sales a float variable storing sales value
*/
public void calcCommission(float sales) {
if (sales > 10000) {
commission = salary * 20 / 100;
} else {
commission = 0;
}
}
/**
* Overloaded method. Calculates commission based on overtime.
*
* @param overtime an integer variable storing overtime hours
*/
public void calcCommission(int overtime) {
if (overtime > 8) {
commission = salary / 30;
} else {
commission = 0;
}
}
/**
* Displays employee details.
*/
public void displayDetails() {
System.out.println("Employee ID: " + empId);
System.out.println("Employee Name: " + empName);
System.out.println("Salary: " + salary);
System.out.println("Commission: " + commission);
}
}
class PartTimeEmployee extends Employee {
String shift; // Variable to store shift information
/**
* Parameterized constructor to initialize values based on user input.
*
* @param id a String variable storing employee ID
* @param name a String variable storing employee name
* @param sal an integer variable storing salary
* @param shift a String variable storing shift information
*/
public PartTimeEmployee(String id, String name, int sal, String shift) {
super(id, name, sal); // Invoke the super class constructor
this.shift = shift;
}
/**
* Overridden method to display employee details.
*/
@Override
public void displayDetails() {
super.displayDetails(); // Invoke the super class display method
System.out.println("Working Shift: " + shift);
}
}
public class EmployeeDetails {
public static void main(String[] args) {
// Instantiate the Employee class object
Employee objEmp = new Employee("E001", "Maria Nemeth", 40000);
objEmp.calcCommission(20000F); // Calculate commission
objEmp.displayDetails(); // Print the details
// Instantiate the Employee object as PartTimeEmployee
Employee objEmp2 = new PartTimeEmployee("E002", "Rob Smith", 30000, "Day");
objEmp2.displayDetails(); // Print the details
}
}
Lớp PartTimeEmployee được kế thừa từ lớp Employee đã được tạo trước đó. Lớp này có biến riêng gọi là shift (ca làm việc) để chỉ định ca làm việc ban ngày hoặc ban đêm cho một nhân viên.
Constructor của lớp PartTimeEmployee gọi constructor của lớp cha bằng cách sử dụng từ khóa super để khởi tạo các thuộc tính chung của một nhân viên. Ngoài ra, nó khởi tạo biến shift (ca làm việc).
Lớp con ghi đè phương thức displayDetails(). Trong phương thức được ghi đè, phương thức calcCommission() được gọi với một đối số kiểu số nguyên. Điều này sẽ tính toán hoa hồng dựa trên làm thêm giờ. Tiếp theo, phương thức displayDetails() của lớp cha được gọi để hiển thị thông tin cơ bản của nhân viên cũng như ca làm việc.
Lớp EmployeeDetails đã được sửa đổi để tạo một đối tượng objEmp1 của lớp Employee. Tuy nhiên, đối tượng này được gán tham chiếu từ lớp PartTimeEmployee và constructor được gọi với bốn đối số. Sau đó, phương thức displayDetails() được gọi để in ra thông tin của nhân viên.
Lưu ý rằng đầu ra của nhân viên E002 hiển thị thông tin của biến ca shift. Điều này cho thấy phương thức displayDetails() của lớp con PartTimeEmployee đã được gọi mặc dù loại đối tượng cho objEmp1 là Employee. Điều này xảy ra vì, trong quá trình tạo đối tượng, nó lưu trữ tham chiếu từ lớp PartTimeEmployee.
Đây là ràng buộc động, tức là cuộc gọi phương thức được ràng buộc với đối tượng tại thời điểm chạy dựa trên tham chiếu được gán cho đối tượng.
Phân biệt giữa Kiểu Tham chiếu (Type Of Reference) và Kiểu Đối tượng (Type Of Object)
Trong ví dụ phần trước kiểu của đối tượng objEmp1 là Employee. Điều này có nghĩa rằng đối tượng sẽ có tất cả các đặc điểm của một Employee. Tuy nhiên, tham chiếu được gán cho đối tượng là của PartTimeEmployee. Điều này có nghĩa là đối tượng sẽ liên kết với các thành viên của lớp PartTimeEmployee trong thời gian chạy (runtime). Đó là, kiểu của đối tượng là Employee và kiểu tham chiếu là PartTimeEmployee. Điều này chỉ có thể xảy ra khi các lớp có một mối quan hệ cha-con.
Trong Java, bạn có thể ép kiểu một thể hiện của một lớp con thành lớp cha của nó. Khái niệm này gọi là upcasting.
Ví dụ
PartTimeEmployee objPT = new PartTimeEmployee();
Employee objEmp = objPT; // Upcasting
// Now, objEmp can only access the members of the Employee class
Trong quá trình upcasting một đối tượng con, đối tượng con objPT được gán trực tiếp cho đối tượng lớp cha objEmp. Tuy nhiên, đối tượng cha không thể truy cập các thành viên cụ thể cho lớp con và không có trong lớp cha.
Java cũng cho phép ép kiểu tham chiếu cha về kiểu con. Điều này xảy ra vì tham chiếu cha trỏ đến một đối tượng kiểu con. Ép kiểu một đối tượng cha thành kiểu con được gọi là downcasting vì một đối tượng đang được ép kiểu thành một lớp thấp hơn trong cấu trúc kế thừa. Tuy nhiên, downcasting yêu cầu phải sử dụng ép kiểu kiểu rõ ràng bằng cách chỉ định tên lớp con trong dấu ngoặc đơn.
Ví dụ:
PartTimeEmployee objPT1 = (PartTimeEmployee) objEmp; // downcasting
Gọi Phương thức Ảo
Trong Code Snippet 7, trong quá trình thực thi câu lệnh Employee objEmp1 = new PartTimeEmployee (…), kiểu dữ liệu trong runtime của đối tượng Employee được xác định. Trình biên dịch không tạo lỗi vì lớp Employee có một phương thức displayDetails(). Tại thời điểm chạy, phương thức được thực thi là phương thức từ đối tượng PartTimeEmployee. Khía cạnh này của đa hình được gọi là gọi phương thức ảo.
Sự khác biệt ở đây là giữa hành vi của trình biên dịch và thời gian chạy. Trình biên dịch kiểm tra tính truy cập của mỗi phương thức và trường dựa trên định nghĩa của lớp, trong khi hành vi liên quan đến một đối tượng được xác định tại thời gian chạy.
Đây là một khía cạnh quan trọng của đa hình, trong đó hành vi của đối tượng được xác định tại thời gian chạy dựa trên tham chiếu được chuyển vào nó.
Do đối tượng được tạo ra thuộc kiểu PartTimeEmployee, phương thức displayDetails() của PartTimeEmployee được gọi ngay cả khi kiểu đối tượng là Employee. Điều này được gọi là gọi phương thức ảo và phương thức này được gọi là phương thức ảo.
Trong Java, tất cả các phương thức hoạt động theo cách này, trong đó một phương thức bị ghi đè trong lớp con sẽ được gọi tại thời điểm chạy, không phụ thuộc vào loại tham chiếu được sử dụng trong mã nguồn tại thời điểm biên dịch. Trong các ngôn ngữ khác như C++, điều tương tự có thể được thực hiện bằng cách sử dụng từ khóa virtual.
Từ khóa abstract (trừu tượng)
Khi thực hiện tính kế thừa, có thể thấy rằng người dùng có thể tạo đối tượng của lớp cha cũng như lớp con. Tuy nhiên, nếu người dùng muốn hạn chế việc sử dụng trực tiếp của lớp cha thì sao? Điều này có nghĩa là, trong một số trường hợp, người ta muốn định nghĩa một lớp cha mà chỉ khai báo cấu trúc của một thực thể cụ thể mà không cung cấp triển khai hoàn chỉnh cho mọi phương thức. Lớp cha như vậy hoạt động như một biểu đồ tổng quan sẽ được kế thừa bởi tất cả các lớp con của nó. Các phương thức của lớp cha phục vụ như một hợp đồng hoặc một tiêu chuẩn mà lớp con có thể triển khai theo cách riêng của nó. Java cung cấp từ khóa abstract để thực hiện công việc này.
Như vậy, một phương thức trừu tượng là một phương thức được khai báo với từ khóa abstract và không có triển khai, tức là không có thân hàm. Phương thức trừu tượng không chứa bất kỳ dấu ngoặc nhọn ‘{}’ nào và kết thúc bằng một dấu chấm phẩy. Cú pháp để khai báo một phương thức trừu tượng như sau:
abstract <return_type> <method_name> (<parameter_list>);
Trong đó:
- abstract: chỉ định rằng phương thức đó là một phương thức trừu tượng.
Ví dụ
public abstract void calculate();
Một lớp trừu tượng (abstract class) là một lớp bao gồm các phương thức trừu tượng. Lớp trừu tượng là một khuôn khổ (framework) cung cấp một số hành vi cho các lớp khác. Các lớp con cung cấp hành vi cụ thể cho khuôn khổ đã có. Lớp trừu tượng không thể được khởi tạo (instantiated), và chúng phải được kế thừa (subclassed) để sử dụng các thành viên của lớp. Lớp con cung cấp triển khai cho các phương thức trừu tượng trong lớp cha. Cú pháp để khai báo một lớp trừu tượng như sau:
abstract class <class-name> {
// Khai báo các trường (fields)
// Định nghĩa các phương thức cụ thể (concrete methods)
// Khai báo các phương thức trừu tượng (abstract methods)
abstract <return_type> <method_name>(<parameter_list>);
}
Trong đó:
- abstract: Cho biết rằng lớp và phương thức là trừu tượng.
Ví dụ:
public abstract class Calculator {
public float getPT() { // Define a concrete method
return 3.147f;
}
abstract void calculate(); // Declare an abstract method
}
Ví dụ tiếp theo, hãy xem triển khai sau:
Chúng ta sẽ làm một ví dụ khởi tạo class trừu tượng Shape và các class Circle, Square, Rectangle kế thừa và triển khai các phương thức trừu tượng tương ứng.
package abstractdemo;
abstract class Shape {
private static final float PI = 3.14F; // Variable to store the value of PI
/**
* Returns the value of PI.
*
* @return float
*/
public float getPI() {
return PI;
}
/**
* Abstract method to calculate something based on a float value.
*
* @param val a float variable storing the value specified by the user
*/
abstract void calculate(float val);
}
Lớp Shape là một lớp trừu tượng với một phương thức cụ thể getPI() và một phương thức trừu tượng calculate().
Để sử dụng lớp trừu tượng này, bạn cần tạo các lớp con. Đoạn code dưới thể hiện hai lớp con Circle và Rectangle kế thừa từ lớp Shape.
package abstractdemo;
class Circle extends Shape {
private float area; // Variable to store the area of a circle
/**
* Implement the abstract method to calculate the area of a circle.
*
* @param rad a float variable storing the value of radius
*/
@Override
void calculate(float rad) {
area = getPI() * rad * rad;
System.out.println("Area of circle is: " + area);
}
}
class Rectangle extends Shape {
private float perimeter; // Variable to store perimeter value
private float length = 10; // Variable to store length
/**
* Implement the abstract method to calculate the perimeter of the rectangle.
*
* @param width a float variable storing width
*/
@Override
void calculate(float width) {
perimeter = 2 * (length + width);
System.out.println("Perimeter of the Rectangle is: " + perimeter);
}
}
Lớp Circle triển khai phương thức trừu tượng calculate() để tính diện tích của một hình tròn. Tương tự, lớp Rectangle triển khai phương thức trừu tượng calculate() để tính chu vi của một hình chữ nhật.
Đoạn mã dưới mô tả mã cho lớp Calculator sử dụng các lớp con dựa trên đầu vào từ người dùng:
package abstractdemo;
public class Calculator {
public static void main(String[] args) {
Shape objShape = null; // Declare the Shape object.
String shape; // Variable to store the type of shape
if (args.length == 2) { // Check the number of command line arguments
// Retrieve the value of shape from args[0]
shape = args[0].toLowerCase(); // Converting to lowercase
switch (shape) {
// Assign reference to Shape object as per user input
case "circle":
objShape = new Circle();
objShape.calculate(Float.parseFloat(args[1]));
break;
case "rectangle":
objShape = new Rectangle();
objShape.calculate(Float.parseFloat(args[1]));
break;
default:
System.out.println("Invalid shape name. Supported shapes: circle, rectangle");
break;
}
} else {
// Error message to be displayed when arguments are not supplied
System.out.println("Usage: java Calculator <shape-name> <value>");
}
}
}
Lớp Calculator lấy hai đối số từ người dùng tại dòng lệnh, đó là hình dạng và giá trị cho hình dạng. Lưu ý đối tượng lớp Shape objShape. Đã được đề cập trước đó rằng một lớp trừu tượng không thể được khởi tạo. Nghĩa là bạn không thể viết Shape objShape = new Shape(). Tuy nhiên, một lớp trừu tượng có thể được gán một tham chiếu của các lớp con của nó.
Dòng lệnh args.length kiểm tra số đối số được cung cấp bởi người dùng. Nếu độ dài là 2, giá trị cho hình dạng được trích xuất từ đối số đầu tiên args[0] và lưu trữ trong biến shape. Sau đó, câu lệnh switch đánh giá giá trị của biến shape và tương ứng gán tham chiếu của hình dạng phù hợp vào đối tượng objshape. Ví dụ, nếu shape là “circle”, nó gán new Circle() làm tham chiếu. Sau đó, sử dụng đối tượng objshape, phương thức calculate() được gọi cho lớp con tương ứng.
Để chạy ví dụ tại dòng lệnh, bạn hãy viết lệnh sau:
java Calculator Rectangle 12
Ví dụ cấu hình NetBean để chạy lệnh này:
Output:
Lưu ý rằng đầu ra hiển thị chu vi của hình chữ nhật. Điều này xảy ra vì đối số dòng lệnh đầu tiên là “Rectangle”. Do đó, trong phương thức main(), trường hợp switch cho hình chữ nhật đã được thực thi.
Tương tự, bạn có thể kế thừa nhiều lớp khác từ lớp trừu tượng Shape và triển khai phương thức calculate() theo cách cần thiết.
Bài tập
Bài 1:
Giả định bạn đã được yêu cầu phát triển một ứng dụng vẽ tranh. Ứng dụng vẽ tranh này nên có khả năng vẽ các loại hình khác nhau như hình tròn, hình vuông, hình bình hành, hình chữ nhật, và nhiều hình dạng khác. Mỗi hình dạng này là một tập hợp của nhiều điểm. Do đó, bạn quyết định tạo một lớp Point trong ứng dụng. Các tính năng cần được triển khai trong lớp Point như sau:
1. Vì một điểm được hình thành bởi hai tọa độ trên mặt phẳng, hãy bao gồm x và y là các trường trong lớp Point.
Triển khai các phương thức, chẳng hạn như setX(), setY(), và displayPoints() trong lớp Point để biểu diễn hành vi của lớp Point.
Triển khai các hàm khởi tạo cần thiết để khởi tạo các đối tượng của lớp Point trong quá trình tạo.
Tạo hai đối tượng kiểu Point và so sánh các tọa độ x và y của chúng. Nếu các tọa độ giống nhau, thì hiển thị 'Các điểm giống nhau', ngược lại, hiển thị 'Các điểm khác nhau'.
Bài 2:
- Tạo abstract class tên Product trong package com.product, với các trường sau:
# |
Data Type |
Name |
Access modifier |
1 |
String |
proId |
protected |
2 |
String |
proName |
protected |
3 |
int |
year |
protected |
4 |
float |
price |
protected |
- Tạo 2 constructor, thêm get/set cho các thuộc tính.
- Tạo 2 abstract methods như sau:
- public abstract void input();
- public abstract void display();
- Tạo class tên Computer trong package product.computers, kế thừa từ class Product and với các thuộc tính:
# |
Data Type |
Name |
Access modifier |
1 |
String |
speed |
private |
2 |
String |
producer |
private |
- Tạo 2 constructors cho class này:
- Không có đối số
- Có 6 đối số, để khởi tạo giá trị được kế thừa và các thuộc tính được thêm vào. Bắt buộc sử dụng từ khóa this để gọi parent construtor có tham số.
- Tạo get/set cho các thuộc tính thêm vào
- Triển khai input() and display() method, được kế thừa từ Product class.
- Input(): cho phép người dùng nhập liệu cho toàn bộ thuộc tính được thêm vào và được kêế thừa của class Computer.
- Display(): In toàn bộ thuộc tính của đối tượng Computer.
- Tạo một class có tên Book trong package product.books, kế thừa class Product và thêm các thuộc tính sau:
# |
Data Type |
Name |
Access modifier |
1 |
String |
type |
private |
2 |
String |
publisher |
private |
- Tạo 2 constructor cho class này:
- Không có đối số
- Có 6 đối số, để khởi tạo toàn bộ thuộc tính cho đối tượng này. Bắt buộc sử dụng từ khóa this để gọi constructor từ class cha.
- Tạo get/set method cho các thuộc tính
- Triển khai input() and display() method, được kế thừa tưừ Product class.
- Input(): cho phép người dùng nhập giá trị cho tất cả các thuộc tính được thêm và được kế thừa của đối tượng Book.
- Display(): n ra tất cả các thuộc tính được thêm và được kế thừa của đối tượng Book..
- Tạo một lớp Test, trong gói product.test, có một phương thức main để chạy ứng dụng. Phương thức main nên thực hiện các nhiệm vụ sau đây:
- Hiển thị Menu cho người dùng để lựa chọn:
Please select: 1. Input information for n Computers. 2. Input information for n Books. 3. Display information of n Computers by sorting the price descending. 4. Display information of n Books by sorting the publisher ascending. 5. Exit. Your choice: |
- Khi người dùng chọn số 1: Nhập thông tin cho n máy tính vào một mảng kiểu Computer
- Khi người dùng chọn số 2: Nhập thông tin cho n cuốn sách vào một mảng kiểu Book
- Khi người dùng chọn số 3: Hiển thị thông tin trong mảng kiểu Computer ở trên
- Khi người dùng chọn số 4: Hiển thị thông tin trong mảng kiểu Book ở trên
- Khi người dùng chọn số 5: Thoát chương trình