NỘI DUNG BÀI VIẾT
Trong toàn bộ bài học về kế thừa và đa hình, chúng ta sẽ học cách thực hiện những việc sau, cũng như khi nào thì nên làm:
- Định nghĩa một lớp con từ một lớp cha thông qua thừa kế.
- Gọi khởi tạo tử và phương thức của lớp cha, thông qua từ khóa
super
. - Cài đè phương thức (và cả khởi tạo tử) của lớp cha.
- Đa hình và trỏ động.
- Làm rõ khái niệm “ép kiểu”, và phân tích ra tại sao việc “ép kiểu tường minh” là cần thiết.
- Cho phép (chỉ các) lớp con sử dụng dữ liệu và phương thức của lớp cha bằng cách sử dụng bổ từ
modifier
- Hạn chế các lớp con mở rộng cũng như cài đè phương thức bằng cách sử dụng bổ từ
final
.
Ngoài ra chúng ta cũng sẽ học về cách:
- Tránh nhầm lẫn giữa
overriding
vàoverloading
(ghi đè và nạp chồng) - Làm rõ phương thức
toString()
mà ta vẫn dùng hằng ngày. - Làm rõ phương thức
equals
mà ta vẫn dùng hằng ngày.
Giới thiệu
Lập trình hướng đối tượng cho phép ta tạo ra các lớp đối tượng mới từ lớp đã tồn tại sẵn. Việc này được gọi tên là “thừa kế”.
Thừa kế là một chức năng quan trọng và mạnh mẽ để có thể viết ra các phần chương trình dễ dàng dùng lại được. Chẳng hạn, bạn cần định nghĩa các lớp để mô phỏng “hình tròn”, “hình chữ nhật” và “tam giác”. Có rất nhiều thuộc tính và hành vi chung giữa các lớp đó. Làm thế nào để viết các lớp đó mà tránh được mã lặp, cũng như dễ nâng cấp bảo trì tất cả cùng một lúc? Câu trả lời nằm ở sự vận dụng thừa kế.
Lớp cha và lớp con
Thừa kế cho phép bạn định nghĩa ra một lớp đối tượng tổng quát (được gọi dưới tên “lớp cha”), và sau đó mở rộng nó thành những lớp đặc trưng hơn (được gọi dưới tên “lớp con”.)
Ta đã biết rằng ta sử dụng lớp đối tượng để lên mô hình những đối tượng thuộc cùng một loại. Trên thực tế có nhiều lớp đối tượng có các thuộc tính và hành vi tổng quát, những thứ được chia sẻ chung nhau giữa nhiều lớp đối tượng. Bạn có thể định ra một lớp đối tượng đặc thù dành cho những thuộc tính tổng quan ấy, và tạo ra các lớp khác mà ngoài việc thừa kế những đặc trưng của lớp cha, còn có những đặc tính mở rộng của riêng chúng.
Hãy xem xét các hình của hình học phẳng, trong tình huống bạn đang xây dựng một ứng dụng cần tới chức năng vẽ hình. Dù gì đi nữa, tất cả các hình vẽ đều có những thuộc tính chung nhau, chúng đều được vẽ viền bởi một màu nào đó và được đổ đầy (hoặc không) bởi một màu nào đó. Bởi vậy mà một lớp tổng quát, Geometric
, có thể được dùng để lên mô hình cho mọi đối tượng hình hình học, lớp này chứa thuộc tính color
và filled
và những getter
và setter
liên quan. Thế là ta có thể làm ra những lớp Cirle
cũng như Rectangle
mà kế thừa những phương thức và thuộc tính đó. Hình dưới đây mô phỏng mối quan hệ giữa các lớp trên. Để ý mũi tên chỉ ra mối quan hệ thừa kế giữa hai lớp đối tượng.
Hãy xem xét từng dữ kiện một, sau đó chúng ta bắt đầu viết mã triển khai sơ đồ. Lưu ý rằng mã triển khai dưới đây có các dòng comment bằng tiếng Việt được thêm vào như một phần của bài đọc, không phải là một phần của mã lệnh.
Geometric.java
public class Geometric { /* các trường dữ liệu */ private String color = "white"; private String filled = null; /* các khởi tạo tử */ public Geometric() { } public Geometric(String color, String filled) { this.color = color; this.filled = filled; } /* các thuộc tính */ public String getColor() { return color; } public void setColor(String color) { this.color = color; } public String getFilled() { return filled; } public void setFilled(String filled) { this.filled = filled; } public String toString() { return "created with \"" + color + "\" color and " + (filled == null ? "no fill" : "filled with \"" + filled + "\" color"); }
Circle.java
Để thông báo rằng lớp Circle
(thừa kế và) mở rộng lớp Geometric
, ta sử dụng từ khóa extends
như dưới đây:
public class Circle extends Geometric { /* mở rộng trường dữ liệu của lớp cha */ private double radius; public Circle() { } public Circle(double radius) { this.radius = radius; } public Circle(double radius, String color, String filled) { this.radius = radius; setColor(color); setFilled(filled); } public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } public double getArea() { return radius * radius * Math.PI; } public double getPerimeter() { return 2 * radius * Math.PI; } public double getDiameter() { return 2 * radius; } public void printCircle() { System.out.println("The " + getColor() + " circle is created with the radius is " + radius); }}
Nhờ thừa kế, lớp Circle
được thừa hưởng những phương thức getColor
, setColor
, getFilled
, setFilled
, và toString
.
Khởi tạo tử chồng Circle(double radius, String color, String filled)
được triển khai bằng cách gọi phương thức setColor
và setFilled
để cài đặt các thuộc tính color
và filled
.
Thử để thấy rằng bạn không thể cài đặt color
và filled
bằng cách sử dụng trực tiếp như sau:
public Circle(double radius, String color, String filled) { this.radius = radius; this.color = color; this.filled = filled; }
Đó là vì dữ liệu private
của lớp Geometric
không thể được truy xuất từ bất kỳ lớp nào ngoại trừ bản thân lớp Geometric
, cách duy nhất để đọc và sửa color
và filled
là thông qua những phương thức getter và setter của chúng.
Rectangle.java
Tương tự như Circle
, chúng ta viết mã triển khai cho lớp Rectangle
:
public class Rectangle extends Geometric { private double width; private double height; public Rectangle() { } public Rectangle(double width, double height) { this.width = width; this.height = height; } public Rectangle(String color, String filled, double width, double height) { this.width = width; this.height = height; setColor(color); setFilled(filled); } public double getWidth() { return width; } public void setWidth(double width) { this.width = width; } public double getHeight() { return height; } public void setHeight(double height) { this.height = height; } public double getArea() { return width * height; } public double getPerimeter() { return 2 * (width + height); }}
Kiểm thử
Mã dưới đây tạo ra những đối tượng của lớp Circle
cũng như Rectangle
và gọi những phương thức của hai lớp đó. Để ý rằng phương thức toString
được cả hai class này thừa hưởng và ta có thể gọi phương thức đó từ những đối tượng của cả hai phương thức.
public class TestCircleRectangle { public static void main(String[] args) { Circle circle = new Circle(1); circle.setFilled("black"); System.out.println("A circle " + circle.toString()); System.out.println("The radius is " + circle.getRadius()); System.out.println("The radius is " + circle.getRadius()); System.out.println("The area is " + circle.getArea()); System.out.println("The diameter is " + circle.getDiameter()); System.out.println(); Rectangle rectangle = new Rectangle(2, 4); System.out.println("A rectangle " + rectangle.toString()); System.out.println("The area is " + rectangle.getArea()); System.out.println("The perimeter is " + rectangle.getPerimeter()); System.out.println(); } }
Kết quả:
A circle created with "white" color and filled with "black" color The radius is 1.0 The radius is 1.0 The area is 3.141592653589793 The diameter is 2.0 A rectangle created with "white" color and no fill The area is 8.0 The perimeter is 12.0
Kết luận
Hết phần này, chúng ta đã lướt qua tình huống thực tế để sử dụng cấu trúc thừa kế, cách triển khai lớp cha và lớp con. Hãy xem xét xem những điểm sau có chính xác không:
- Trái với cách nghĩ thông thường, lớp con không phải là một phần của lớp cha, trái lại, lớp con thậm chí còn thường chứa nhiều thông tin và phương thức hơn cả lớp cha của nó.
- Trường dữ liệu
private
trong lớp cha không thể được truy cập từ bên ngoài lớp đó, bởi vậy cũng không thể truy cập được từ lớp con. Tuy nhiên, không hạn chế nếu lớp con truy cập những trường đó thông qua các phương thức mà lớp cha đãpublic
ra ngoài. - Không phải mọi quan hệ kiểu “cái này là cái kia” đều nên được mô phỏng thông qua mối quan hệ thừa kế. Ví dụ một hình vuông là một hình chữ nhật, nhưng không nên mở rộng lớp hình chữ nhật để có lớp hình vuông, vì những thuộc tính
width
vàheight
là không thích hợp cho hình vuông. Thay vào đó, lớp hình vuông nên mở rộng lớp hình hình học và mở rộng thêm với thuộc tínhside
. - Thừa kế được sử dụng để mô hình hóa mối quan hệ
cái này là một cái kia
. Do đó đừng mù quáng sử dụng thừa kế chỉ để tái sử dụng các mã đã có sẵn. Ví dụ không có nghĩa lý gì khi mở rộng lớp Cây thành lớp Con người chỉ vì lớp cây đã có sẵn hành vi trao đổi chất, hay như vì hai lớp đấy có sẵn một số thuộc tính như chiều cao và trọng lượng. Một lớp con và lớp cha của nó phải có mối quan hệ là một. - Một vài ngôn ngữ cho phép một lớp được mở rộng từ nhiều lớp, khả năng này được gọi là đa thừa kế. Tuy nhiên Java không cho phép đa thừa kế. Một lớp ở Java chỉ được kế thừa trực tiếp từ một lớp cha, đặc tính này được gọi là đơn kế thừa. Mặc dù vậy, khả năng mở rộng từ nhiều nguồn mô phỏng vẫn đạt được thông qua các giao diện, sẽ được trình bày trong một bài học khác.
Phản biện
Bạn đồng thuận với cách sử dụng thuật ngữ nào hơn? Lớp con/lớp cha hay phân lớp/siêu lớp?