Tính đa hình trong java – Ví dụ và bài tập mẫu

Đ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ẽ áp dụng tính đa hình trong java vào chương trình quản lý học viện.

Xem thêm:

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 là kỹ thuật 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 gọi phương thức 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.

Nó giống như việc bạn được chỉ định để thực hiện nhiệm vụ đó bởi một người quản lý. Bạn nhận thấy có một cách để thực hiện công việc tốt hơn so với cách ban đầu. Bạn quyết định áp dụng cách của mình và thông báo cho người quản lý.

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.”

  }

}

3.2 Overloading (Ghi chồng)

Overloading là kỹ thuật tạo ra nhiều phương thức cùng tên nhưng khác nhau về số lượng hoặc kiểu dữ liệu của tham số. Các phương thức được gọi tương ứng với các tham số truyền vào. Môt ví dụ thực tế để bạn dễ hiểu hơn là máy giặt có nhiều chế độ giặt 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 khác nhau như giặt nhanh, giặt êm, giặt bằng nước nóng để giặt các loại quần áo khác nhau.

class HinhChuNhat {
  double tinhDienTich(double chieuDai, double chieuRong) {

    return chieuDai * chieuRong;

  }

  double tinhDienTich(double canh) {

    return canh * canh;

  }

}

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 animal1animal2 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 CatDog. 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. Bài tập 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:

Nguyễn Tiến Trường

Mình viết về những điều nhỏ nhặt trong cuộc sống, Viết về câu chuyện những ngày không có em