Java Bài 30: Đa Hình (Polymorphism)

Posted by

Chào mừng các bạn đã đến với bài học Java số 30. Bài học về tính đa hình (polymorphism). Đây là bài học trong chuỗi bài viết về lập trình ngôn ngữ Java của Yellow Code Books.

Bài hôm nay chúng ta sẽ nói sâu về tính Đa hình trong Java. Nghe qua đặc tính này thì có vẻ khó. Một phần vì ứng dụng của chúng không nhiều. Với cái tên nghe chẳng có cố định gì cả, như là biến hình gì gì đó. Cộng với khá ít tài liệu viết rõ về công năng này của OOP.

Vậy thì chúng ta cùng đi sâu vào bài học để xem Đa hình là gì và nó có thực sự khó không nhé.

Tính Đa Hình (Polymorphism) Là Gì?

Lần này thì nghĩa tiếng Anh và tiếng Việt trong lập trình Java lại khớp với nhau. Không nhiều các từ lan man, chỉ có Đa hình, hoặc Polymorphism mà thôi.

Vậy tại sao lại Đa hình? Như bạn biết, vốn dĩ OOP là một cách thức tư duy lập trình hướng thực tế, nên hiển nhiên các khái niệm của nó cũng phải sát với các đặc điểm trong thực tế. Trong đó có Đa hình. Trong thực tế, sự Đa hình được xem như một đối tượng đặc biệt, có lúc đối tượng này mang một hình dạng (trở thành một đối tượng) nào đó, và cũng có lúc đối tượng này lại mang một hình dạng khác nữa, tùy vào từng hoàn cảnh. Sự “nhập vai” vào các hình dạng (đối tượng) khác nhau này giúp cho đối tượng Đa hình ban đầu có thể thực hiện những hành động khác nhau của từng đối tượng cụ thể. Chẳng hạn nếu ở công ty bạn, có nhân viên nhận hai trách nhiệm khác nhau, họ vừa là nhân viên toàn thời gian ở các ngày trong tuần, nhưng làm bán thời gian ở các ngày cuối tuần. Vậy thì, để tính lương cho nhân viên này, tùy vào từng thời điểm mà hệ thống sẽ xem nhân viên đó là toàn thời gian hay bán thời gian, và phương thức tính lương của mỗi loại nhân viên sẽ thực hiện tính toán một cách hiệu quả nhất dựa vào từng vai trò khác nhau này. Bạn cũng hiểu sơ sơ về Đa hình rồi đúng không nào.

Có một điều chắc chắn rằng. Nếu như không xem hành động tính lương của nhân viên như ví dụ trên kia là Đa hình, thì chúng ta vẫn cứ xây dựng được một hệ thống tính lương hoàn chỉnh, nhưng sẽ phức tạp hơn là nếu bạn biết kiến thức về Đa hình là gì.

Và còn một ý nữa. Rằng tính Đa hình của bài hôm nay cũng là một trong các đặc tính nổi trội mà OOP mang lại đấy nhé. Bạn cố gắng nắm bắt và tận dụng. Ôn lại một tí các đặc tính cốt lõi của OOP bao gồm:

– Tính Gói ghém dữ liệu (Encapsulation). Tính chất này được thể hiện qua các kiến thức về khả năng truy cập, getter/setter.
– Tính Kế thừa (Inheritance). Tính chất này được thể hiện qua các kiến thức về kế thừa, overriding, overloading.
– Tính Đa hình (Polymorphism). Bài hôm nay chúng ta sẽ học.
– Tính Trừu tượng (Abstraction). Bài sau chúng ta sẽ học.

Sử Dụng Tính Đa Hình Như Thế Nào?

Đến đây chắc chắn bạn đã hiểu sơ bộ khái niệm Đa hình. Vậy thì trong OOP chúng ta tổ chức và sử dụng đặc tính Đa hình này như thế nào?

Thứ nhất, Đa hình sẽ gắn liền với kế thừa. Và, Đa hình cũng sẽ gắn liền với ghi đè phương thức (overriding) nữa. Bởi vì như trên đây có nói đó, Đa hình là nói đến một đối tượng nào đó có khả năng nhập vai thành các đối tượng khác. Vậy thì để mà một đối tượng có thể là một đối tượng nào đó, ắt hẳn nó phải là đối tượng cha. Và để đối tượng cha có thể là một trong các đối tượng con ở từng hoàn cảnh, thì nó phải định nghĩa ra các phương thức để con của nó có thể ghi đè. Điều này giúp hệ thống xác định được đối tượng nào và phương thức nào thực sự đang hoạt động khi ứng dụng đang chạy. Nên nhiều tài liệu gọi Đa hình này là Đa hình tại runtime là vậy.

Chúng ta sẽ đến ví dụ sau cho dễ hiểu hơn. Ví dụ khá đơn giản. Lớp HinhHoc là lớp cha, hai lớp con HinhTronHinhChuNhat đều override phương thức tinhDienTich() từ cha.

Đa hình - Sơ đồ lớp ví dụ

Code của chúng cũng khá đơn giản, chúng ta loại bỏ hết tất cả các râu ria khác, chỉ tập trung vào các phương thức override mà thôi.

HinhHoc

public class HinhHoc {
	
	public void tinhDienTich() {
		System.out.println("Chưa biết hình nào");
	}
}

HinhTron

public class HinhTron extends HinhHoc {
	
	@Override
	public void tinhDienTich() {
		System.out.println("Đây là Diện tích hình Tròn");
	}
 
}

HinhChuNhat

public class HinhChuNhat extends HinhHoc {
 
	@Override
	public void tinhDienTich() {
		System.out.println("Đây là Diện tích hình Chữ nhật");
	}

}

Nào, sự diệu kỳ của tính Đa hình là đây, bạn hãy chú ý vào đoạn code khai báo và sử dụng các phương thức được overriding trên kia như sau.

MainClass

public class MainClass {
 
    public static void main(String[] args) {
        HinhHoc hinhHoc = new HinhHoc();
        hinhHoc.tinhDienTich(); // Đoạn code này bình thường, sẽ in ra "Chưa biết hình nào"
        
        // Có lúc hinhHoc đóng vai trò là HinhTron trong một ngữ cảnh nào đó
        hinhHoc = new HinhTron();
        hinhHoc.tinhDienTich(); // Đoạn code này sẽ in ra "Đây là Diện tích Hình tròn"
        
        // Có lúc hinhHoc đóng vai trò là HinhChuNhat trong một ngữ cảnh nào đó
        hinhHoc = new HinhChuNhat();
        hinhHoc.tinhDienTich(); // Đoạn code này sẽ in ra "Đây là Diện tích Chữ nhật"
    }
 
}

Bạn đã thấy đó, đối tượng HinhHoc bản thân nó có một phương thức tinhDienTich(). Nhưng khác với cách sử dụng các đối tượng từ các bài học từ trước đến giờ, rằng mỗi khi cần đến các lớp con thực hiện việc tính diện tích, chúng ta sẽ khai báo lớp con và gọi phương thức được override ở lớp con. Thì bài hôm nay chúng ta cho phép lớp HinhHoc có khả năng đóng vai trò là lớp con, bằng cách khởi tạo lại đối tượng là lớp con của nó, HinhHoc hinhHoc = new HinhTron(), rồi chính nó sẽ đóng vai là lớp con đó. Tính Đa hình là đây.

Thực Hành Xây Dựng Ứng Dụng Tính Lương Cho Nhân Viên

Nếu như ở bài học trước chúng ta đã xây dựng “hoàn chỉnh” một hệ thống tính lương “phức tạp” cho một công ty “bự”. Nhưng code khi đó lại không mang rõ tính ứng dụng thực tế, bởi vì chúng ta đã code “cứng” ở chỗ biết trước anh nhân viên nào là lính, anh nào là sếp, anh nào làm toàn thời gian, anh nào làm bán thời gian, để mà khai báo các đối tượng NhanVienFullTime hay NhanVienPartTime tương ứng.

Vậy sang bài hôm nay, chúng ta sẽ hoàn thiện ứng dụng tính lương nhân viên của bài trước. Làm cho hệ thống trở nên thực tế hơn. Cụ thể, bài này chúng ta sẽ cho người dùng nhập bằng tay thông tin nhân viên. Và vì vậy sẽ có một mảng các nhân viên trong ứng dụng. Lớp NhanVien sẽ là lớp có sử dụng đặc tính Đa hình để có thể đóng vai trò là NhanVienFullTime hoặc NhanVienPartTime ở từng hoàn cảnh cụ thể.

Mô Tả Lại Yêu Cầu Chương Trình

Yêu cầu của chương trình tính lương không hề thay đổi so với bài trước. Mình chỉ mô tả lại thôi.

– Công ty có hai loại nhân viên: nhân viên toàn thời gian và nhân viên thời vụ.
– Nhân viên toàn thời gian là lính sẽ hưởng lương 10 củ một tháng. Nhân viên toàn thời gian là sếp sẽ hưởng lương 20 củ một tháng.
– Nhân viên toàn thời gian nếu làm thêm ngày nào thì sẽ được cộng thêm 800k mỗi ngày, bất kể chức vụ.
– Nhân viên thời vụ cứ làm mỗi giờ được 100k, không phân biệt chức vụ gì cả. Làm nhiều thì hưởng nhiều.

Ứng dụng sẽ cho phép người dùng nhập vào số lượng nhân viên. Sau đó với từng nhân viên, người dùng phải nhập vào tên nhân viên, loại nhân viên toàn thời gian hay bán thời gian, nhân viên toàn thời gian thì là nhân viên lính hay nhân viên sếp, có làm thêm ngày nào không, nhân viên thời vụ thì làm được mấy giờ. Cuối cùng dựa vào các thông tin đó, sẽ xuất ra màn hình lương tương ứng cho tất cả nhân viên.

Sơ Đồ Lớp

Chúng ta vẫn dựa vào sơ đồ lớp của bài trước. Nhưng chỉnh sửa một chút sao cho lớp NhanVien sẽ “vào vai” tốt các lớp con của nó. Bằng cách xây dựng thêm phương thức tinhLuong() ở lớp này, rồi ở các lớp con sẽ phải override lại.

Đa hình - Sơ đồ lớp bài thực hành

Xây Dựng Các Lớp

Lớp Configs không hề thay đổi.

package util;

public class Configs {

	// Loại nhân viên
	public static final int NHAN_VIEN_SEP = 1;
	public static final int NHAN_VIEN_LINH = 2;
	
	// Lương nhân viên
	public static final long LUONG_NHAN_VIEN_FULL_TIME_SEP = 20000000; // Lương tháng của sếp
	public static final long LUONG_NHAN_VIEN_FULL_TIME_LINH = 10000000; // Lương tháng của lính
	public static final long LUONG_LAM_THEM_MOI_NGAY = 800000; // Làm thêm mỗi ngày của nhân viên toàn thời gian được 800 k
	public static final long LUONG_NHAN_VIEN_PART_TIME_MOI_GIO = 100000; // Lương nhân viên thời vụ mỗi giờ 100 k
}

Lớp NhanVien chỉ có thêm phương thức tinhLuong() để thực hiện Đa hình trên phương thức này.

package model;

public class NhanVien {

	protected String ten;
	protected long luong;
	
	public NhanVien() {	
	}
	
	public NhanVien(String ten) {
		this.ten = ten;
	}
	
	protected String loaiNhanVien() {
		// Lớp con phải override để lo vụ loại nhân viên này
		return "";
	}
	
	public void tinhLuong() {
		// Lớp con phải override để lo vụ tính lương này
	}
	
	public void xuatThongTin() {
		System.out.println("===== Nhân viên: " + ten + " =====");
		System.out.println("- Loại nhân viên: " + loaiNhanVien());
		System.out.println("- Lương: " + luong + " VND");
	}
}

Lớp NhanVienFullTimeNhanVienPartTime cũng không thay đổi gì. Chỉ có giảm bớt overloadingconstructor để khâu nhập liệu được dễ dàng hơn thôi.

package model;

import util.Configs;

/**
 * NhanVienFullTime chính là nhân viên toàn thời gian
 */
public class NhanVienFullTime extends NhanVien {
	
	private int ngayLamThem; // Ngày làm thêm của nhân viên
	private int loaiChucVu; // Chức vụ là lính hay sếp
	
	public NhanVienFullTime(String ten, int ngayLamThem, int loaiChucVu) {
		super(ten);
		this.ngayLamThem = ngayLamThem;
		this.loaiChucVu = loaiChucVu;
	}
	
	@Override
	public String loaiNhanVien() {
		if (loaiChucVu == Configs.NHAN_VIEN_LINH) {
			return "Lính toàn thời gian" + (ngayLamThem > 0 ? " (có làm thêm ngày)":"");
		} else {
			return "Sếp toàn thời gian" + (ngayLamThem > 0 ? " (có làm thêm ngày)":"");
		}
	}
	
	@Override
	public void tinhLuong() {
		if (loaiChucVu == Configs.NHAN_VIEN_LINH) {
			luong = Configs.LUONG_NHAN_VIEN_FULL_TIME_LINH + ngayLamThem * Configs.LUONG_LAM_THEM_MOI_NGAY;
		} else if (loaiChucVu == Configs.NHAN_VIEN_SEP) {
			luong = Configs.LUONG_NHAN_VIEN_FULL_TIME_SEP + ngayLamThem * Configs.LUONG_LAM_THEM_MOI_NGAY;
		}
	}
}
package model;

import util.Configs;

/**
 * NhanVienPartTime chính là nhân viên thời vụ
 */
public class NhanVienPartTime extends NhanVien {
	
	private int gioLamViec; // Tổng số giờ làm việc của nhân viên
	
	public NhanVienPartTime(String ten, int gioLamViec) {
		this.ten = ten;
		this.gioLamViec = gioLamViec;
	}
	
	@Override
	public String loaiNhanVien() {
		return "Nhân viên thời vụ";
	}
	
	@Override
	public void tinhLuong() {
		luong = Configs.LUONG_NHAN_VIEN_PART_TIME_MOI_GIO * gioLamViec;
	}
}

Và đây. Mọi thay đổi sẽ nằm ở phương thức main(). Nếu bạn đừng để ý đến các đoạn code râu ria nhập liệu từ console. Thì phương thức main() có các ý sau chúng ta nên lưu tâm.

– Lần đầu tiên, chúng ta sử dụng đến mảng các đối tượng. Và bạn thấy rằng, mảng các đối tượng cũng chẳng khác mảng của các kiểu nguyên thủy mà bạn đã học là mấy.
– Với mỗi phần tử trong mảng các NhanVien. Chúng ta khởi tạo NhanVien này là NhanVienFullTime hay NhanVienPartTime là do điều kiện mà người dùng nhập vào. Tính Đa hình phát huy tác dụng ở đây.

package main;

import java.util.Scanner;

import model.NhanVien;
import model.NhanVienFullTime;
import model.NhanVienPartTime;

public class MainClass {

	public static void main(String[] args) {
		// Kêu người dùng nhập vào số lượng nhân viên trong công ty
		Scanner scanner = new Scanner(System.in);
		System.out.print("Hãy nhập số lượng nhân viên: ");
		int tongNhanVien = Integer.parseInt(scanner.nextLine());
		
		// Khai báo mảng các nhân viên
		NhanVien[] mangNhanVien = new NhanVien[tongNhanVien];
		for (int i = 0; i < tongNhanVien; i++) {
			// Khai báo từng loại nhân viên, và kêu người dùng nhập thông tin nhân viên
			System.out.print("Tên nhân viên " + (i + 1) + ": ");
			String ten = scanner.nextLine();
			System.out.print("Là nhân viên (1-Toàn thời gian; 2-Bán thời gian): ");
			int laNhanVien = Integer.parseInt(scanner.nextLine());
			if (laNhanVien == 1) {
				// Nhân viên toàn thời gian
				System.out.print("Chức vụ nhân viên (1-Sếp; 2-Lính): ");
				int chucVu = Integer.parseInt(scanner.nextLine());
				System.out.print("Ngày làm thêm (nếu có): ");
				int ngayLamThem = Integer.parseInt(scanner.nextLine());
				mangNhanVien[i] = new NhanVienFullTime(ten, ngayLamThem, chucVu);
			} else {
				System.out.print("Giờ làm: ");
				int gioLamViec = Integer.parseInt(scanner.nextLine());
				mangNhanVien[i] = new NhanVienPartTime(ten, gioLamViec);
			}
		}
		
		System.out.println("\nKết quả tính lương\n");
		
		// Tính lương và xuất thông tin nhân viên
		for (NhanVien nhanVien : mangNhanVien) {
			nhanVien.tinhLuong();
			nhanVien.xuatThongTin();
		}
	}

}

Cuối cùng là kết quả thực thi chương trình. Ngoài việc nhập liệu động ra thì kết quả in ra là như bài hôm trước. Bạn thử kết hợp việc nhập liệu động của bài hôm nay với việc không sử dụng tính Đa hình, mà dùng như bài học hôm trước xem. Với cách thử nghiệm này, bạn sẽ hiểu rõ hơn về thế mạnh của Đa hình đấy.

Đa hình - Kết quả console bài thực hành

Đa hình là vậy, bạn có thấy khó không nào. Sẽ còn kiến thức có liên quan đến Đa hình nữa, như ép kiểu trong OOP chẳng hạn, mà chúng ta sẽ nói ở bài học sau.

Cảm ơn bạn đã đọc các bài viết của Yellow Code Books. Bạn hãy đánh giá 5 sao nếu thấy thích bài viết, hãy comment bên dưới nếu có thắc mắc, hãy để lại địa chỉ email của bạn để nhận được thông báo mới nhất khi có bài viết mới, và nhớ chia sẻ các bài viết của Yellow Code Books đến nhiều người khác nữa nhé.

Bài Kế Tiếp

Chúng ta đã biết đến ép kiểu đối với kiểu dữ liệu nguyên thủy rồi đúng không nào. Vậy thì ép kiểu với các đối tượng OOP có gì khác không. Bài sau chúng ta cùng tìm hiểu nhé.

Advertisements
Rating: 5.0/5. From 6 votes.
Please wait...

Gửi phản hồi