Đa hình (polymorphism) là một khái niệm quan trọng trong lập trình hướng đối tượng. Trong bài viết này, chúng ta sẽ làm ví dụ tính đa hình trong java thông qua chương trình quản lý học viện.
Xem thêm:
- Viết chương trình quản lý cán bộ java
- Bài thực hành 12: tính kế thừa trong java – bài tập có lời giải
1. Tính đa hình (polymorphism) trong Java là gì?
1.1 Tính đa hình là gì?
Tính đa hình (polymorphism) xuất phát từ tiếng Hy Lạp, có nghĩa là “nhiều hình dạng”. Ý chỉ khả năng mà một đối tượng có thể nhận nhiều hình thức khác nhau. Ví dụ thực tế, cùng là một chiếc điện thoại, nhưng có thể sử dụng để gọi điện, nhắn tin, chụp ảnh, nghe nhạc, v.v. Đây chính là biểu hiện của tính đa hình
Trong lập trình, đa hình giúp một đối tượng có thể thực hiện một tác vụ theo nhiều cách khác nhau.
1.2 Tính đa hình trong Java là gì?
Trong Java, tính đa hình cho phép một biến có thể trỏ đến các đối tượng khác nhau, miễn là chúng kế thừa từ một lớp chung. Điều này giúp chúng ta có thể xây dựng các hệ thống phần mềm linh hoạt và dễ mở rộng hơn.
Giả sử, chúng ta có một biến tham chiếu như nv1
, nv2
, nv3
,… có thể trỏ đến đối tượng của lớp ConNguoi
hoặc lớp NhanVien
, thì chúng ta có thể gọi phương thức thongTin
của cả lớp ConNguoi
và lớp NhanVien
thông qua biến tham chiếu đó.
2. Cú pháp và cách sử dụng tính đa hình
Để sử dụng tính đa hình, chúng ta sử dụng từ khóa “extends” để tạo ra một lớp con kế thừa từ lớp mẹ. Từ đó, ghi đè (override) các phương thức của lớp mẹ trong lớp con. Sau đó, ta có thể khởi tạo đối tượng từ lớp con nhưng tham chiếu bằng một biến thuộc lớp mẹ. Ví dụ: Animal animal = new Dog()
. Đây là cách chúng ta sử dụng đối tượng Dog như là một đối tượng Animal.
3. Phương thức để đạt tính đa hình trong Java
Vậy làm sao để đạt tính đa hình trong Java? Chúng ta sẽ sử dụng 2 phương thức chính sau:
3.1 Overriding (Ghi đè)
Overriding (Ghi đè) là một kỹ thuật trong lập trình hướng đối tượng, cho phép lớp con định nghĩa lại phương thức của lớp cha với cùng tên, cùng tham số, và cùng kiểu trả về. Khi phương thức này được gọi thông qua đối tượng của lớp con, phương thức của lớp con sẽ được thực thi thay vì phương thức của lớp cha.
Bạn có thể hình dung như thế này: Khi bạn được giao một nhiệm vụ bởi quản lý, bạn nhận thấy rằng có một cách tốt hơn để hoàn thành công việc so với cách ban đầu. Bạn quyết định áp dụng cách của mình và thực hiện nó, thay thế cho phương pháp cũ.
class ConNguoi { void hienThi() { System.out.println(“Tôi là con người.”); } } class NhanVien extends ConNguoi { void hienThi() { System.out.println(“Tôi là nhân viên.”); } } public class Main { public static void main(String[] args) { ConNguoi conNguoi = new NhanVien(); conNguoi.hienThi(); // In ra: “Tôi là nhân viên.” } }
Trong ví dụ này, khi gọi phương thức hienThi()
từ đối tượng NhanVien
, phương thức ghi đè của lớp con (NhanVien
) sẽ được thực thi thay vì phương thức của lớp cha (ConNguoi
)
Xem thêm: chương trình tính lương nhân viên java
3.2 Overloading (Ghi chồng)
Overloading (Nạp chồng) là kỹ thuật cho phép tạo ra nhiều phương thức cùng tên trong cùng một lớp, nhưng khác nhau về số lượng hoặc kiểu dữ liệu của tham số. Khi gọi phương thức, hệ thống sẽ chọn phương thức phù hợp dựa trên các tham số được truyền vào.
Hãy tưởng tượng như thế này: Máy giặt của bạn có nhiều chế độ giặt khác nhau, như giặt nhanh, giặt êm, hoặc giặt bằng nước nóng để giặt các loại quần áo khác nhau. Máy giặt sử dụng kỹ thuật nạp chồng (overloading) để cung cấp cho bạn nhiều tùy chọn dựa trên nhu cầu.
class HinhChuNhat { double tinhDienTich(double chieuDai, double chieuRong) { return chieuDai * chieuRong; } double tinhDienTich(double canh) { return canh * canh; } }
Trong ví dụ này, lớp HinhChuNhat
có hai phương thức tinhDienTich
cùng tên, nhưng một phương thức tính diện tích hình chữ nhật (dựa vào chiều dài và chiều rộng), còn phương thức kia tính diện tích hình vuông (dựa vào độ dài của cạnh). Khi gọi phương thức với tham số phù hợp, phương thức tương ứng sẽ được thực thi.
Xem thêm:
3.3 Sự khác nhau giữa overloading và overriding trong java
Overloading(Nạp chồng) | Overriding(Ghi đè) |
Phương thức nạp chồng được sử dụng để tăng tính có thể đọc của chương trình. | Phương thức ghi đè được sử dụng để cung cấp trình triển khai cụ thể của phương thức mà đã được cung cấp bởi lớp cha của nó |
Được thực hiện bên trong lớp class. | Xuất hiện trong hai lớp mà có mối quan hệ IS-A (kế thừa) |
Tham số phải là khác nhau | Tham số phải là giống nhau |
Ví dụ của đa hình tại biên dịch(compile). | Ví dụ của đa hình tại thực thi (runtime) |
4. Phân loại đa hình trong Java
Đa hình trong Java được chia làm 2 loại chính là đa hình Compile time (thời điểm biên dịch) và đa hình runtime (thời điểm chạy). Với mỗi loại đa hình này sẽ có phương thức thực hiện khác nhau.
4.1 Đa hình Compile -Time trong Java
Đa hình compile time (thời điểm biên dịch) trong Java còn được gọi là đa hình tĩnh (static). Vì nó quyết định về phương thức nào sẽ được gọi tại thời điểm biên dịch, dựa trên kiểu của biến tham chiếu được sử dụng để gọi phương thức. Điều này xảy ra khi chúng ta sử dụng các phương thức được định nghĩa ở lớp cha hoặc interface và triển khai lại chúng trong các lớp con. Đa hình biên dịch có thể được đạt được thông qua quá trình nạp chồng phương thức (Method Overloading).
Ví dụ:
public class Example { public void display(int num) { System.out.println(“Number: ” + num); } public void display(String text) { System.out.println(“Text: ” + text); } public void display(int num1, int num2) { System.out.println(“Numbers: ” + num1 + “, ” + num2); } }
Ở ví dụ trên, chúng ta có ba phương thức display() có cùng tên nhưng có tham số khác nhau. Tại thời điểm biên dịch, Java sẽ xác định phương thức nào sẽ được gọi dựa trên thông tin về tham số truyền vào.
Lưu ý: Ngoài method overloading, tính đa hình nói chung còn có thể đạt được bằng operator overloading. Tuy nhiên, Java không hỗ trợ operator overloading nên chúng ta sẽ không đi sâu về vấn đề này.
4.2 Đa hình Runtime trong Java
Đa hình Runtime (thời điểm chạy) hay đa hình động (dynamic) trong Java là việc gọi phương thức được được ghi đè trong thời gian chạy chương trình. Điều này có nghĩa là máy ảo Java (JVM) quyết định xem sử dụng cài đặt của phương thức nào dựa trên kiểu thực tế của đối tượng tại thời điểm chạy, xảy ra khi quyết định. Đa hình runtime đạt được thông qua việc ghi đè phương thức (overriding).
Ở đây, ghi đè được xác định bằng cách sử dụng một biến tham chiếu của lớp cha. Phương thức sẽ được gọi dựa trên đối tượng xác định bởi biến tham chiếu. Điều này còn được gọi là upcasting (quá trình chuyển đổi kiểu dữ liệu đối tượng từ lớp con sang lớp cha).
Lưu ý: Đa hình Runtime chỉ có thể được đạt được thông qua phương thức ghi đè, không phải thông qua biến do biến tham chiếu đã được xác định tại Compile time và không thể thay đổi tại run-time.
public class Animal { public void makeSound() { System.out.println(“Animal is making a sound”); } } public class Cat extends Animal { @Override public void makeSound() { System.out.println(“Meow!”); } } public class Dog extends Animal { @Override public void makeSound() { System.out.println(“Woof!”); } } public class Main { public static void main(String[] args) { Animal animal1 = new Cat(); Animal animal2 = new Dog(); animal1.makeSound(); // Output: “Meow!” animal2.makeSound(); // Output: “Woof!” } }
Ở ví dụ trên, chúng ta tạo ra hai đối tượng animal1
và animal2
với kiểu dữ liệu là lớp cha Animal
, nhưng thực sự được khởi tạo là đối tượng của lớp con Cat
và Dog
. Khi gọi phương thức makeSound()
trên hai đối tượng này, phương thức được ghi đè trong lớp con sẽ được thực thi.
4.3 Đa hình Runtime trong Java với kế thừa đa tầng
Kế thừa đa tầng là cho phép một lớp con kế thừa từ một lớp cha, và lớp cha này lại kế thừa từ một lớp khác. Ví dụ, lớp con Dog
kế thừa từ lớp cha Animal
, và lớp cha Animal
kế thừa từ lớp cha LivingBeing
.
Khi kết hợp đa hình runtime với kế thừa đa tầng. Chúng ta có thể sử dụng tính đa hình để gọi phương thức của đối tượng đang được xác định bởi biến tham chiếu của lớp cha. Đối tượng này có thể thuộc lớp con của lớp cha đó hoặc thuộc một lớp cha khác nằm trong chuỗi kế thừa đa tầng.
Ví dụ:
class LivingBeing { public void eat() { System.out.println(“This living being is eating”); } } class Animal extends LivingBeing { @Override public void eat() { System.out.println(“This animal is eating”); } } class Dog extends Animal { @Override public void eat() { System.out.println(“This dog is eating”); } } public class Main { public static void main(String[] args) { LivingBeing lb1 = new LivingBeing(); LivingBeing lb2 = new Animal(); LivingBeing lb3 = new Dog(); lb1.eat(); // This living being is eating lb2.eat(); // This animal is eating lb3.eat(); // This dog is eating } }
Khi gọi phương thức eat()
thông qua biến tham chiếu lb1
của lớp cha LivingBeing
, phương thức eat()
của LivingBeing
sẽ được gọi. Khi gọi phương thức eat()
thông qua biến tham chiếu lb2
của lớp con Animal
, phương thức eat()
của Animal
sẽ được gọi. Và khi gọi phương thức eat()
thông qua biến tham chiếu lb3
của lớp con Dog
, phương thức eat()
của Dog
sẽ được gọi.
5. Tại sao sử dụng tính đa hình trong Java?
Đa hình (Polymorphism) trong Java cho phép viết một phương thức có thể xử lý chính xác nhiều loại chức năng khác nhau có cùng tên. Chúng ta cũng có thể đạt được tính nhất quán trong mã của mình bằng cách sử dụng đa hình.
5.1 Ưu điểm của đa hình trong Java
- Nó cung cấp khả năng tái sử dụng cho mã. Chúng ta có thể tận dụng các lớp và phương thức đã được định nghĩa trước để triển khai các chức năng mới mà không cần viết lại mã nguồn từ đầu. Ngoài ra, người lập trình có thể thay đổi mã mà không ảnh hưởng đến mã gốc.
- Cho phép tạo ra các lớp con có tính chất riêng của chúng và sử dụng chúng trong một cấu trúc kế thừa phức tạp hơn.
- Với ít dòng mã hơn, người lập trình dễ dàng hơn trong việc sửa lỗi mã.
5.2 Nhược điểm của tính đa hình trong Java
- Tính đa hình có thể làm tăng độ phức tạp của mã nguồn và khó hiểu hơn, đặc biệt là khi sử dụng nhiều lớp con kế thừa từ cùng một lớp cha.
- Tính đa hình có thể làm giảm hiệu suất của chương trình trong một số trường hợp. Khi chúng ta sử dụng đa hình, Java phải tìm kiếm phương thức thích hợp để thực thi, điều này có thể làm giảm hiệu suất.
Tuy nhiên chúng ta vẫn có cải thiện nhược điểm của tính đa hình trong Java bằng việc sử dụng các các thiết kế mẫu (design patterns), áp dụng các kỹ thuật và các công cụ hỗ trợ khác như Kotlin, Scala hoặc IDE như Eclipse,…
6. Ví dụ tính đa hình java có lời giải
Bài 11: Xây dựng chương trình quản lý dùng cho một học viện. Đối tượng quản lý bao gồm các sinh viên, nhân viên, khách hàng.
– Lớp Person: bao gồm các thuộc tính họ tên, địa chỉ, phương thức toString.
– Các lớp Students, Employee, Customer (mô tả dưới đây) thừa kế lớp Person.
+ Lớp Students: bao gồm các thuộc tính điểm môn học 1, điểm môn học 2. Các phương thức: tính điểm trung bình, đánh giá. Overriding phương thức toString trả về bảng điểm sinh viên (gồm thông tin thuộc tính và điểm TB).
+ Lớp Employee: bao gồm thuộc tính heSoLuong và các phương thức: tính lương, đánh giá. Overriding phương thức toString trả về bảng lương cho nhân viên (gồm thông tin thuộc tính đối tượng và tiền lương).
+ Lớp Customer: bao gồm thuộc tính tên công ty, trị giá hoá đơn, đánh giá. Phương thức toString trả về thông tin hoá đơn cho khách hàng (gồm các thuộc tính của đối tượng).
– Lớp DanhSach có 1 biến danh sách để lưu các sinh viên, nhân viên, khách hàng. Dùng 1 biến array Person, biến lưu tổng số người có trong danh sách, constructor mặc định khởi tạo array với dung lượng cho trước.
- Phương thức thêm một người vào danh sách (thống số Person),
- Xoá 1 người khỏi danh sách (nhận thông số là họ tên người cần xoá),
- Sắp xếp danh sách theo thứ tự họ tên,
- Phương thức xuất danh sách.
- Khi danh sách đầy thì tự động tăng dung lượng dãy lên 50%.
– Viết lớp với phương thức main cho phần kiểm nghiệm. Giao tiếp với người dùng bằng menu(thể hiện tính đa hình – polymorphism bằng cách cho phép lựa chọn nhập thông tin là sinh viên, nhân viên hay khách hàng).
6.1 Code tham khảo
– Class Person:
package bai11; import java.util.Scanner; public class Person { String hoten; String diachi; public Person(){ } public Person(String hoten, String diachi){ this.hoten= hoten; this.diachi=diachi; } public String getHoten() { return hoten; } public void setHoten(String hoten) { this.hoten = hoten; } public String getDiachi() { return diachi; } public void setDiachi(String diachi) { this.diachi = diachi; } public void nhap(){ Scanner sc= new Scanner(System.in); System.out.println("Nhap ho va ten: "); hoten= sc.nextLine(); System.out.println("Nhap dia chi: "); diachi= sc.nextLine(); } public void xuat(){ System.out.println("Ho ten: "+ getHoten()); System.out.println("Dia chi: "+ getDiachi()); } }
– Class Students:
package bai11; import java.util.Scanner; public class Student extends Person{ float diem1, diem2; public Student(){ } public Student(float diem1, float diem2, String hoten, String diachi){ super(hoten, diachi); this.diem1=diem1; this.diem2=diem2; } public float getDiem1() { return diem1; } public void setDiem1(float diem1) { this.diem1 = diem1; } public float getDiem2() { return diem2; } public void setDiem2(float diem2) { this.diem2 = diem2; } public void nhapStu(){ Scanner sc= new Scanner(System.in); super.nhap(); System.out.println("Nhap diem 1: "); diem1= sc.nextFloat(); System.out.println("Nhap diem 2: "); diem2= sc.nextFloat(); } float diemTB(){ return (diem1+diem2)/2; } String danhGia(){ if(diemTB()>=8&& getDiem1()>=6.5&& getDiem2()>=6.5){ return "Gioi"; }else if(diemTB()<8&& getDiem1()>=6.5&& getDiem2()>=6.5&& diemTB()>=6.5){ return "Kha"; }else if(diemTB()>=5 && diemTB()<6.5){ return "Trung Binh"; }else return "Yeu"; } public void xuat(){ System.out.println("===Student==="); super.xuat(); System.out.println("Diem 1: "+ getDiem1()); System.out.println("Diem 2: "+ getDiem2()); System.out.println("Diem trung binh"+ diemTB()); System.out.println("Danh gia: "+ danhGia()); System.out.println("======"); } }
– Class Employee:
package bai11; import java.util.Scanner; public class Employee extends Person{ float hsl, lcb=1000; public Employee(){ } public Employee(float hsl, String hoten, String diachi){ super(hoten, diachi); this.hsl=hsl; } public float getHsl() { return hsl; } public void setHsl(float hsl) { this.hsl = hsl; } public void nhapEmp(){ Scanner sc = new Scanner(System.in); super.nhap(); System.out.println("Nhap he so luong: "); hsl=sc.nextFloat(); } float tinhLuong(){ return hsl*lcb; } String danhGiaEmp(){ if(getHsl()==2){ return "Trung Binh"; }else if(getHsl()<2){ return "Thap"; }else return "Cao"; } public void xuat(){ System.out.println("===Employee==="); super.xuat(); System.out.println("He so luong: "+ getHsl()); System.out.println("Luong: "+tinhLuong()); System.out.println("Danh Gia: "+ danhGiaEmp()); System.out.println("======"); } }
– Class Customer:
package bai11; import java.util.Scanner; public class Customer extends Person{ String tenCT; float trigiaHD; public Customer(){ } public Customer(String tenCT, float trigiaHD, String hoten, String diachi){ super(hoten, diachi); this.tenCT=tenCT; this.trigiaHD=trigiaHD; } public String getTenCT() { return tenCT; } public void setTenCT(String tenCT) { this.tenCT = tenCT; } public float getTrigiaHD() { return trigiaHD; } public void setTrigiaHD(float trigiaHD) { this.trigiaHD = trigiaHD; } public void nhapCus(){ Scanner sc = new Scanner(System.in); super.nhap(); System.out.println("Nhap ten cong ty: "); tenCT= sc.nextLine(); System.out.println("Nhap tri gia hoa don: "); trigiaHD= sc.nextFloat(); } public void xuat(){ System.out.println("===Customer==="); super.xuat(); System.out.println("Ten cong ty: "+getTenCT()); System.out.println("Tri gia hoa don: "+ getTrigiaHD()); System.out.println("======"); } }
– Class DanhSach:
package bai11; import java.util.ArrayList; import java.util.Scanner; public class DanhSach { int chon,a,b,c; ArrayList<Person> list = new ArrayList<>(); ArrayList<Student> listS = new ArrayList<>(); ArrayList<Employee> listE = new ArrayList<>(); ArrayList<Customer> listC = new ArrayList<>(); Scanner sc= new Scanner(System.in); public void menu(){ System.out.println("1. Nhap danh sach Hoc Sinh"); System.out.println("2. Nhap danh sach Nhan vien"); System.out.println("3. Nhap sanh sach Khach hang"); System.out.println("4. Xuat danh sach"); System.out.println("5. Xoa nguoi theo ten"); System.out.println("0.Thoat"); } public void nhapDSStu(){ Scanner sc = new Scanner(System.in); System.out.println("Nhap so hoc sinh muon nhap: "); a= sc.nextInt(); listS = new ArrayList(a); for(int i=0;i<a;i++){ Student stu = new Student(); stu.nhapStu(); list.add(stu); } } public void nhapDSEmp(){ System.out.println("Nhap so nhan vien muon nhap: "); b= sc.nextInt(); listE = new ArrayList(b); for(int i=0;i<b;i++){ Employee emp = new Employee(); emp.nhapEmp(); list.add(emp); } } public void nhapDSCus(){ System.out.println("Nhap so khach hang muon nhap: "); c= sc.nextInt(); listC = new ArrayList(c); for(int i=0;i<c;i++){ Customer cus = new Customer(); cus.nhapCus(); list.add(cus); } } public void nhapDS(){ do { menu(); System.out.println("Lua chon: "); chon= sc.nextInt(); switch(chon){ case 1: nhapDSStu();break; case 2: nhapDSEmp(); break; case 3: nhapDSCus(); break; case 4: xuatDS(); break; case 5: xoaNguoi(); break; case 0: System.exit(0);break; } } while (chon!=0); } public void xuatDS(){ for(int i=0;i<list.size();i++){ list.get(i).xuat(); } } public void xoaNguoi(){ Scanner sc = new Scanner(System.in); String ten; System.out.println("Nhap ten nguoi can xoa: "); ten= sc.nextLine(); for(int i=0;i<list.size();i++){ if(list.get(i).hoten.equals(ten)){ list.remove(i); } } } }
– Class main:
package bai11; import java.util.Scanner; public class Bai11 { public static void main(String[] args) { int chon; DanhSach a = new DanhSach(); a.nhapDS(); } }
6.2 Kết quả
Source code FULL:
Hy vọng bài viết này đã cung cấp các thông tin cần thiết về tính đa hình trong Java. Cảm ơn các bạn đã tham khảo bài tập lập trình hướng đối tượng java trên ttnguyen.net.
Bài viết liên quan: