Java Bài 21: Làm Quen Với Kế Thừa

Posted by

Được chỉnh sửa ngày 28/6/2017.

Chào mừng các bạn đến với bài học Java thứ 21 trong chuỗi bài học về lập trình ngôn ngữ Java của Yellow Code Books.

Vậy là chúng ta đã bước qua lần lượt nhiều kiến thức quan trọng trong lập trình hướng đối tượng, như thuộc tính, phương thức, constructor. Nhưng có một loại kiến thức có thể nói là tinh hoa của hướng đối tượng, mà chúng ta sẽ tiếp cận bắt đầu từ bài học hôm nay, sẽ làm bạn có một cách sử dụng và tổ chức các lớp trong ứng dụng theo một cách thức hoàn toàn nâng cao và hiệu quả hơn so với các cách mà bạn đã làm quen từ các bài học trước, đó là kiến thức về Kế thừa.

Nếu bạn là người đang tập làm quen với Java và hướng đối tượng, thì kế thừa sẽ khiến bạn càng thêm khó khăn hơn một chút. Đến lúc này, Java code không còn quan trọng bằng việc bạn tổ chức cấu trúc cho các lớp hay đối tượng bên trong ứng dụng nữa. Theo kinh nghiệm của mình, thì sau khi làm quen đến kế thừa, sẽ có vô số thắc mắc mang đến với các bạn. Có bạn sẽ thắc mắc không biết khi nào nên kế thừa. Cũng có bạn thì hiểu rõ kế thừa nhưng lại tổ chức kế thừa sai “họ hàng” của chúng, khiến việc kế thừa trở nên rối hơn. Rồi thì khi nào nên chặn tính kế thừa của bất kỳ một lớp nào đó. Vân vân và vân vân. Những thắc mắc đó mình sẽ cố gắng trình bày kỹ càng trong loạt bài viết về kế thừa này, để bạn có một cách thức vận dụng tốt nhất tính kế thừa vào trong sản phẩm của bạn.

Làm Quen Với Kế Thừa

Kế Thừa Là Gì?

Kế thừa trong lập trình hướng đối tượng ám chỉ đến một mối quan hệ giữa các đối tượng, có người thì nói mối quan hệ này là Cha-Con, có người thì nói là quan hệ Mở rộng. Người ta có vẻ thích cái khái niệm Cha-Con hơn, nhưng mình thấy Mở rộng, và sát nhất nên hiểu là Kế thừa luôn, thì sẽ hay hơn. Bởi vì nó thực chất là một sự dùng lại, một số trường hợp là mở rộng hơn các đặc tính, của một đối tượng từ một đối tượng nào đó khác.

Như vậy, để hiểu một cách thực tế, giả sử đầu tiên chúng ta có một lớp nào đó, lớp này có thể là do chúng ta viết ra, hay “lượm lặt” ở đâu đó, mình tạm gọi tên lớp sẵn có này là A. Sau đó, để tận dụng lại các hàm hay các thuộc tính của A mà không cần phải viết lại (hoặc copy lại, có thể vi phạm bản quyền), thì chúng ta xây dựng một lớp mới kế thừa từ A, mình gọi lớp mới này là B. Khi đó B của chúng ta sẽ có sẵn các phương thức và thuộc tính mà A có. Cũng có lúc không vì mục đích dùng lại các giá trị của A, mà là vì một vài giá trị của A không phù hợp với nhu cầu của B, thế là việc kế thừa từ A còn giúp cho B có cơ hội được hoàn thiện lại (hay còn gọi là mở rộng) các giá trị chưa phù hợp đó của A mà không làm thay đổi bản chất của A. Hôm nay chúng ta tập trung vào mục đích Dùng lại, mục đích Mở rộng mình sẽ nói đến ở bài học sau.

Tại Sao Phải Kế Thừa?

Qua các ý trên đây của mình, có lẽ các bạn cũng đã hiểu lý do tại sao phải kế thừa đúng không nào.

Vâng, mục đích chính mà kế thừa mang lại, đó là việc tận dụng lại, và mở rộng hơn các thuộc tính và phương thức có sẵn từ một đối tượng nào đó.

Vậy thôi, mục đích của kế thừa không cao siêu gì cả, nhưng tác dụng mà nó mang lại là rất to lớn. Với việc dùng lại những cái sẵn có này, sẽ làm cho cấu trúc project của bạn trông chuyên nghiệp hơn, khi đó code của bạn sẽ dễ đọc hơn. Ngoài ra thì việc kế thừa còn giúp cho bạn giảm bớt gánh nặng phải code nhiều, vì đã tận dụng lại code đã có của các lớp khác.

Kế Thừa Trong Java

Nãy giờ chúng ta đang nói chung chung về các khái niệm. Vậy thì làm sao để thể hiện sự kế thừa trong Java? Trong Java, để thể hiện một lớp muốn kế thừa từ một lớp nào đó, bạn sử dụng từ khóa extends.

Bạn hãy nhìn vào ví dụ dưới đây, bạn đừng nên code vội, lát nữa vào phần thực hành chúng ta sẽ cùng code.

class HinhTron {
	float bk;
	float getBanKinh() {
		return bk;
	}
}

class HinhTru extends HinhTron {

}

Code trên đây là thể hiện rõ ràng cho sự kế thừa, trong đó lớp HinhTru kế thừa HinhTron bằng từ khóa extends. Và theo quy luật kế thừa, thì HinhTru có thể sẽ thừa hưởng các giá trị (thuộc tính và phương thức) mà HinhTron đã khai báo. Qua mối quan hệ kế thừa như vậy, người ta có thể gọi lớp HinhTron là lớp Cơ sở (Base Class), hay lớp Cha (Parent Class). Còn lớp HinhTru được gọi là lớp Dẫn xuất (Derived Class) hay lớp Con (Subclass, Child Class).

Thông thường thì các lớp cha, hay lớp cơ sở, là các lớp chứa đựng các giá trị chung, hay các giá trị cơ sở nhất cho các lớp con. Nên nếu như bạn có nhiều lớp có sự tương đồng nhất định, như Hình tròn và Hình trụ ở ví dụ trên, các lớp này đều có mặt tròn (hình trụ có hai mặt tròn), nên bạn có thể dùng Hình tròn làm lớp cơ sở (vì nó chứa các giá trị tối thiểu mà Hình trụ có thể tận dụng lại được, trong trường hợp này chính là mặt tròn). Hoặc có những trường hợp có nhiều các lớp có cùng các giá trị tương đồng, mà bạn có thể gom thành một lớp cơ sở duy nhất, rồi các lớp con chỉ việc kế thừa và sử dụng lại các giá trị tương đồng đó mà không cần phải khai báo gì thêm, như bài thực hành bên dưới mình sẽ tạo một lớp HinhHoc là cơ sở nhất cho các lớp HinhTron, HinhVuong, HinhChuNhat,…

Trên đây là một ví dụ cho việc khi nào thì bạn cần kế thừa, còn bây giờ mình xin liệt kê một số ý quan trọng trong quá trình bạn tổ chức kế thừa.

Một lớp chỉ được phép kế thừa từ một và chỉ một lớp cha mà thôi.

– Tuy không được kế thừa từ nhiều lớp cha, nhưng lớp cha mà đối tượng đang kế thừa này có thể kế thừa từ một lớp cha khác, bạn có thể gọi chơi lớp cha khác này là “lớp ông nội” cho dễ nhớ, và chắc chắc có thể có cả “lớp ông cố” nữa nếu như “lớp ông nội” lại kế thừa một lớp khác nữa.

– Nếu một lớp không khai báo kế thừa gì hết (như các lớp mà chúng ta đã thực hành từ các bài học trước), thì khi này hệ thống sẽ mặc định xem là nó đang kế thừa từ lớp Object. Bài học sau chúng ta sẽ cùng nói rõ về lớp Object này nhé.

Làm Quen Với Sơ Đồ Lớp

Khái niệm kế thừa ở bài học hôm nay chỉ như vậy thôi. Nhưng trước khi đi vào thực hành cụ thể, mình mời các bạn cùng làm quen với một sơ đồ thần thánh, mà nếu là một dân lập trình chính thống, bạn không thể không biết đến. Cái chính của mục này là nói về cách thức để bạn nhìn và hiểu sơ đồ. Vì project của chúng ta ngày một phức tạp, sẽ có nhiều và rất nhiều lớp, chúng sử dụng nhau, kế thừa nhau. Và vì vậy mà nếu không có sơ đồ, chúng ta sẽ không thể nào diễn tả hết được các mối quan hệ này bằng lời.

Sơ đồ lớp của mục này được mình lấy ra từ nguyên tắc xây dựng Sơ đồ lớp (Class Diagram) của UML. UML là một bộ nhiều các nguyên tắc khác nhau dành cho việc đặc tả và thiết kế hệ thống phần mềm. Nói như vậy để bạn hiểu đây là một sơ đồ tuân thủ theo các nguyên tắc chuẩn, nếu bạn hiểu và tuân thủ các nguyên tắc này, bạn sẽ tạo ra một mô hình có tiếng nói chung, mà ai đọc vào cũng hiểu bạn muốn thể hiện gì cho phần mềm của bạn.

Bạn biết không, bạn đã từng làm quen một chút với sơ đồ này rồi, vì mình đã từng dùng qua ở bài 16, khi đó mình muốn diễn đạt một lớp có ba thành phần chính như sau, và hình khối mà bạn trông thấy chính là cách mà sơ đồ lớp biểu diễn ra một thực thể lớp.

Blank Diagram - Page 2

Hình dáng và màu sắc của các khối trong sơ đồ lớp có thể khác nhau ở bài học của mình và ở các tài liệu khác, tùy vào công cụ để vẽ nó. Nhưng cho dù chúng khác nhau về ngoại hình, thì chung quy lại các dữ liệu mà mỗi sơ đồ thể hiện phải đều tuân thủ một nguyên tắc hình khối với ba thành phần trên.

Mình lấy ví dụ lớp HinhTron như code trên kia, thì khi biểu diễn thành một thực thể trong sơ đồ lớp, sẽ trông như thế này.

hinhtron

Nhìn vào sơ đồ, bạn biết ngay cần xây dựng một lớp có tên HinhTron, lớp này có một thuộc tính tên bk có kiểu dữ liệu là float, và một phương thức getBanKinh() trả về kiểu float. Tuy mang giá trị tĩnh, tức là nó không thể hiện được nội dung hay mối quan hệ của các thành phần bên trong một lớp, nhưng chắc hẳn bạn cũng dễ hiểu và hình dung ra được cách thức xây dựng một lớp dựa trên sơ đồ này là như thế nào.

Tiếp theo, mình nói tiếp sơ đồ lớp này thể hiện sự kế thừa như thế nào. Với mong muốn lớp HinhTru kế thừa từ HinhTron, người ta thể hiện qua sơ đồ lớp mối quan hệ này bằng một dấu mũi tên như sau. Bạn chú ý phải là dấu mũi tên rỗng ruột như hình, nếu bạn dùng mũi tên kiểu khác, thì sẽ gây nhầm lẫn với các mối quan hệ khác đấy nhé.

hinhtron_hinhtru

Một ý nữa của sơ đồ, ví dụ như trường hợp ở bài 18, lớp HinhTron có sử dụng lớp ToaDo để làm một thuộc tính, thuộc tính này có tên là toaDo, vậy thì sơ đồ sẽ thể hiện sự sử dụng này như sau.

hinhtron_hinhtru_toado

Mọi thứ thật sự rõ ràng đúng không nào. Xong rồi, với bài học hôm nay mình chỉ trình bày sơ lược về thừa kế và về sơ đồ lớp như vậy thôi. Chúng ta sẽ bổ sung các kiến thức, các ký hiệu cho sơ đồ này ở các bài học tiếp theo. Còn bây giờ chúng ta cần thực hành cho quen tay.

Thực Hành Kế Thừa

Chúng ta sẽ tiếp tục cùng nhau xây dựng ứng dụng tính toán các giá trị hình học cho HinhTron, HinhTru, HinhChuNhat, HinhVuong. Bạn cũng nên biết là, nếu không áp dụng kiến thức về kế thừa của bài hôm nay, thì bạn vẫn xây dựng được kết quả của bài thực hành này một cách hoàn hảo bằng các kiến thức về OOP ở các bài trước, bạn có thể thử. Nhưng với việc áp dụng tính kế thừa, như mình có nói, bạn sẽ tiết kiệm được các dòng code một cách đáng kể. Vậy thì mời bạn cùng thử xây dựng một project mới với mình nhé.

Trước hết mình mời bạn xem qua sơ đồ lớp của bài hôm nay (có áp dụng kế thừa) như sau, bạn hãy nghiền ngẫm một tí nhé (chú ý các dấu + ở trước mỗi phương thức hay thuộc tính trong sơ đồ là các khai báo với từ khóa public, đây là khả_năng_truy_cập vào các giá trị lớp mà chúng ta sẽ nói sau).

hinhhoc

Trước khi đi vào chính thức, mình sẽ nhìn sơ đồ và nói chi tiết từng lớp, rồi cho bạn xem kết quả chạy chương trình, và cuối cùng là code của từng lớp. Bạn khoan hãy xem code của các lớp vội, mà hãy dựa vào sơ đồ và kết quả rồi thử code nhé, mỗi người sẽ có một cách code khác nhau, bạn không nhất thiết phải code giống như mình, miễn sao chương trình của bạn chạy ổn là được.

– Lớp HinhHoc. Đây là lớp cha của các lớp còn lại, hay mang ý nghĩa là lớp cơ bản nhất. Do là lớp cơ bản, nên tốt nhất nó phải chứa các thuộc tính hay phương thức mà sẽ hữu dụng cho các lớp con, hay có thể nói rằng lớp con hoàn toàn có thể kế thừa lại các giá trị đó một cách hiệu quả. Chẳng hạn hằng số PI mình sẽ khai báo ở lớp này. Thuộc tính ten tuy dùng chung nhưng sẽ được các lớp con định nghĩa cụ thể theo tên của chúng. Các chuVi, dienTich, theTich cũng vậy, tuy định nghĩa chung nhưng các lớp con sẽ chứa các giá trị khác nhau. Các phương thức của lớp cha này cũng mang ý nghĩa sẽ được các lớp con dùng đến, nên chúng sẽ có thân hàm cụ thể, như xuatTen() sẽ xuất biến ten ra consolse. Hay inChuVi(), inDienTich(), inTheTich() cũng sẽ xuất các biến chuVi, dienTich, theTich tương ứng.

– Lớp HinhTron. Là lớp con của HinhHoc. Như bạn biết HinhTron sẽ kế thừa các giá trị từ lớp cha của nó. Ngoài các thuộc tính mà nó có được từ lớp cha là ten, chuVi, dienTich, theTich, thì nó cũng định nghĩa thêm một thuộc tính banKinh đặc biệt của riêng nó. Bạn có thể khởi tạo biến ten cho HinhTron ở constructor HinhTron(). Các phương thức nhapBanKinh(), tinhChuVi(), tinhDienTich() không khai báo ở lớp cha, HinhTron tự nó thiết kế.

– Lớp HinhTru. Lớp này là con của HinhTron, bởi như mình nói trên kia, HinhTru có các mặt tròn, mặt tròn này không khác gì các đặc tính của một HinhTron, nên HinhTron nên là một lớp cơ bản của HinhTru. Chính vì HinhTru kế thừa các giá trị từ HinhTron, mà HinhTron kế thừa từ HinhHoc, nên HinhTru có đủ hết các giá trị của HinhHocHinhTron. Nó chỉ cần thêm thuộc tính chieuCao, và các phương thức nhapChieuCao(), tinhTheTich() của riêng nó nữa mà thôi.

– Tương tự cho các mối quan hệ của HinhChuNhatHinhVuong. Có một điều đặc biệt ở lớp HinhVuong, đó là vì các cạnh dài và rộng của hình này bằng nhau, nên hàm nhapCanh() của hình vuông chỉ kêu người dùng nhập một cạnh, sau đó bạn gán cùng giá trị cạnh này cho các biến dairong, và thế là HinhVuong không cần phải xây dựng thêm các phương thức nào cả, kế thừa hoàn toàn xuất sắc từ lớp cha của nó.

Kết quả chạy chương trình giống giống như sau.

console_21

Về phần project, mình tạo ra một project mới tên là InheritanceLearning (OOPLearningPackageLearning đã bị close lại). Với cách tổ chức các lớp vào trong các package như sau.

Screen Shot 2017-06-14 at 06.30.38

Sau đây là source code của các lớp tương ứng trong chương trình cho bạn tham khảo.

Lớp HinhHoc.

package shapes;

public class HinhHoc {

	public final float PI = 3.14f;

	public String ten;
	public float chuVi;
	public float dienTich;
	public float theTich;

	public void xuatTen() {
		System.out.println("\n\n===== " + ten + " =====");
	}

	public void inChuVi() {
        System.out.println("Chu vi = " + chuVi);
    }

    public void inDienTich() {
        System.out.println("Diện tích = " + dienTich);
    }

    public void inTheTich() {
    	System.out.println("Thể tích = " + theTich);
    }
}

Lớp HinhTron.

package shapes;

import java.util.Scanner;

public class HinhTron extends HinhHoc {

	public float banKinh;

	// Constructor
	public HinhTron() {
		ten = "Hình Tròn";
	}

    public void nhapBanKinh() {
        System.out.println("Bán kính = ");
        Scanner scanner = new Scanner(System.in);
        banKinh = scanner.nextFloat();
    }

    public void tinhChuVi() {
        chuVi = 2 * PI * banKinh;
    }

    public void tinhDienTich() {
        dienTich = PI * banKinh * banKinh;
    }

}

Lớp HinhTru.

package shapes;

import java.util.Scanner;

public class HinhTru extends HinhTron {

	public float chieuCao;

	// Constructor
	public HinhTru() {
		ten = "Hình Trụ";
	}

	public void nhapChieuCao() {
		nhapBanKinh();

		System.out.println("Chiều cao = ");
        Scanner scanner = new Scanner(System.in);
        chieuCao = scanner.nextFloat();
	}

	public void tinhTheTich() {
		tinhDienTich();
        theTich = dienTich * chieuCao;
    }
}

Lớp HinhChuNhat.

package shapes;

import java.util.Scanner;

public class HinhChuNhat extends HinhHoc {

	public float dai;
    public float rong;

    // Constructor
    public HinhChuNhat() {
    	ten = "Hình Chữ Nhật";
    }

    public void nhapChieuDai() {
        System.out.println("Chiều dài = ");
        Scanner scanner = new Scanner(System.in);
        dai = scanner.nextFloat();
    }

    public void nhapChieuRong() {
        System.out.println("Chiều rộng = ");
        Scanner scanner = new Scanner(System.in);
        rong = scanner.nextFloat();
    }

    public void tinhChuVi() {
        chuVi = 2 * (dai + rong);
    }

    public void tinhDienTich() {
        dienTich = dai * rong;
    }

}

Lớp HinhVuong.

package shapes;

import java.util.Scanner;

public class HinhVuong extends HinhChuNhat {

	// Constructor
    public HinhVuong() {
    	ten = "Hình Vuông";
    }

    public void nhapCanh() {
    	System.out.println("Cạnh = ");
        Scanner scanner = new Scanner(System.in);
        dai = rong = scanner.nextFloat();
    }
}

Và cuối cùng là lớp MainClass.

package main;

import shapes.HinhChuNhat;
import shapes.HinhTron;
import shapes.HinhTru;
import shapes.HinhVuong;

public class MainClass {

	public static void main(String[] args) {
		// Thử nghiệm với lớp Hình tròn
		HinhTron hinhTron = new HinhTron();
		hinhTron.xuatTen();
		hinhTron.nhapBanKinh();
		hinhTron.tinhChuVi();
		hinhTron.tinhDienTich();
		hinhTron.inChuVi();
		hinhTron.inDienTich();

		// Thử nghiệm với lớp Hình trụ
		HinhTru hinhTru = new HinhTru();
		hinhTru.xuatTen();
		hinhTru.nhapChieuCao();
		hinhTru.tinhTheTich();
		hinhTru.inTheTich();

		// Thử nghiệm với lớp Hình chữ nhật
		HinhChuNhat hinhChuNhat = new HinhChuNhat();
		hinhChuNhat.xuatTen();
		hinhChuNhat.nhapChieuDai();
		hinhChuNhat.nhapChieuRong();
		hinhChuNhat.tinhChuVi();
		hinhChuNhat.tinhDienTich();
		hinhChuNhat.inChuVi();
		hinhChuNhat.inDienTich();

		// Thử nghiệm với lớp Hình vuông
		HinhVuong hinhVuong = new HinhVuong();
		hinhVuong.xuatTen();
		hinhVuong.nhapCanh();
		hinhVuong.tinhChuVi();
		hinhVuong.tinhDienTich();
		hinhVuong.inChuVi();
		hinhVuong.inDienTich();
	}

}

Đấy bạn thấy càng ngày càng phải code nhiều rồi đúng không nào. Cái chính của việc học lập trình đó là bạn đừng ngại code, tuy kiến thức về kế thừa hôm nay giúp ích cho bạn khá nhiều về việc tiết kiệm các dòng code, nhưng không phải vì vậy mà các lập trình viên chúng ta hoàn toàn rảnh rỗi.

Bài tuy nhiều code, nhưng hi vọng các bạn hứng thú, và sẵn sàng để cùng mình đi qua các kiến thức “đau đầu” hơn (nhưng thú vị hơn) về OOP ở các bài sắp tới.

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

Bài học sau chúng ta sẽ nói qua cách sử dụng hai từ khóa thissuper, hai từ khóa này sẽ giúp ích gì cho mối quan hệ kế thừa này, mời các bạn cùng đọc xem nhé.

Advertisements
Rating: 5.0. From 3 votes.
Please wait...

5 comments

  1. Anh trai ơi , đọc bài anh em dễ hiểu lắm … em đã có thể tự code được bải như trên … nhưng khi so với source code của a thì chỉ thiếu mỗi public ở phần khai báo các thuộc tính . nhưng sao em chạy chương trình thì nó báo như này ạ : Error: Could not find or load main class main.MainClass
    C:\Users\danht\AppData\Local\NetBeans\Cache\8.2\executor-snippets\run.xml:53: Java returned: 1
    BUILD FAILED (total time: 0 seconds)
    A giúp em với

    1. Chắc bạn Quang dùng Netbeans đúng không. Nhìn đường dẫn project của bạn sao thấy lạ quá, nó nằm trong catche của Netbeans. Bạn thử copy project ra ổ đĩa khác thử xem, ổ E:\ chẳng hạn. Hoặc chuyển sang dùng Eclipse như các bài viết của mình cho chắc cú.

Gửi phản hồi