2. Inheritance
Trong OOP, chúng tôi thường tổ chức các lớp theo thứ bậc để tránh trùng lặp và giảm sự dư thừa . Các lớp trong hệ thống phân cấp thấp hơn kế thừa tất cả các biến (thuộc tính tĩnh) và phương thức (hành vi động) từ cấu trúc phân cấp cao hơn. Một lớp trong hệ thống phân cấp thấp hơn được gọi là một lớp con (hoặc dẫn xuất , lớp con , lớp mở rộng ). Một lớp trong hệ thống phân cấp trên được gọi là siêu lớp (hoặc lớp cơ sở , lớp cha ). Bằng cách kéo ra tất cả các biến và phương thức phổ biến vào các siêu lớp và để lại các biến và phương thức chuyên biệt trong các lớp con, dự phòngcó thể được giảm hoặc loại bỏ rất nhiều vì các biến và phương thức phổ biến này không cần phải lặp lại trong tất cả các lớp con. Ví dụ,
Một lớp con kế thừa tất cả các biến và phương thức từ các siêu lớp của nó, bao gồm cả cha mẹ trực tiếp của nó cũng như tất cả các tổ tiên. Điều quan trọng cần lưu ý là một lớp con không phải là "tập hợp con" của siêu lớp. Ngược lại, lớp con là "siêu lớp" của siêu lớp. Đó là bởi vì một lớp con kế thừa tất cả các biến và phương thức của lớp cha; Ngoài ra, nó mở rộng siêu lớp bằng cách cung cấp nhiều biến và phương thức hơn.
Trong Java, bạn định nghĩa một lớp con bằng cách sử dụng từ khóa " extends
", ví dụ:
class Goalkeeper extends SoccerPlayer {......}
class MyApplet extends java.applet.Applet {.....}
class Cylinder extends Circle {......}
Ký hiệu UML: Ký hiệu UML cho thừa kế là một đường liền nét với đầu mũi tên rỗng dẫn từ lớp con đến siêu lớp của nó. Theo quy ước, siêu lớp được vẽ trên đầu các lớp con của nó như được hiển thị.
2.1 Tính Thừa Kế EG. 1: Lớp Circle và lớp Cylinder
Trong ví dụ này, chúng ta có một lớp con Cylinder
thừa kế từ lớp cha Circle
, mà chúng ta đã tạo trong chương trước. Điều quan trọng cần lưu ý là chúng tôi sử dụng lại lớp Circle
. Khả năng sử dụng lại là một trong những tính chất quan trọng nhất của OOP. (Tại sao phát minh lại bánh xe?) Lớp Cylinder
kế thừa tất cả các biến thành viên ( radius
và color
) và phương thức ( getRadius()
, getArea()
, và những thứ khác nghĩa) từ lớp cha của nó Circle
. Nó tiếp tục định nghĩa một biến gọi là height
, hai phương thức - getHeight()
và getVolume()
và constructor riêng của nó, như:
Circle.java (Re-produced)
public class Circle {
private double radius;
private String color;
public Circle() {
this.radius = 1.0;
this.color = "red";
System.out.println("Construced a Circle with Circle()");
}
public Circle(double radius) {
this.radius = radius;
this.color = "red";
System.out.println("Construced a Circle with Circle(radius)");
}
public Circle(double radius, String color) {
this.radius = radius;
this.color = color;
System.out.println("Construced a Circle with Circle(radius, color)");
}
public double getRadius() {
return this.radius;
}
public String getColor() {
return this.color;
}
public void setRadius(double radius) {
this.radius = radius;
}
public void setColor(String color) {
this.color = color;
}
public String toString() {
return "Circle[radius=" + radius + ",color=" + color + "]";
}
public double getArea() {
return radius * radius * Math.PI;
}
}
Cylinder.java
public class Cylinder extends Circle {
private double height;
public Cylinder() {
super();
this.height = 1.0;
System.out.println("Constructed a Cylinder with Cylinder()");
}
public Cylinder(double height) {
super();
this.height = height;
System.out.println("Constructed a Cylinder with Cylinder(height)");
}
public Cylinder(double height, double radius) {
super(radius);
this.height = height;
System.out.println("Constructed a Cylinder with Cylinder(height, radius)");
}
public Cylinder(double height, double radius, String color) {
super(radius, color);
this.height = height;
System.out.println("Constructed a Cylinder with Cylinder(height, radius, color)");
}
public double getHeight() {
return this.height;
}
public void setHeight(double height) {
this.height = height;
}
public String toString() {
return "This is a Cylinder";
}
}
Test Drive cho Lớp Cylinder (TestCylinder.java)
public class TestCylinder {
public static void main(String[] args) {
Cylinder cy1 = new Cylinder();
System.out.println("Radius is " + cy1.getRadius()
+ ", Height is " + cy1.getHeight()
+ ", Color is " + cy1.getColor()
+ ", Base area is " + cy1.getArea()
+ ", Volume is " + cy1.getVolume());
Cylinder cy2 = new Cylinder(5.0, 2.0);
System.out.println("Radius is " + cy2.getRadius()
+ ", Height is " + cy2.getHeight()
+ ", Color is " + cy2.getColor()
+ ", Base area is " + cy2.getArea()
+ ", Volume is " + cy2.getVolume());
}
}
Lưu ý ta cần đặt 2 lớp "Cylinder.java
" và "TestCylinder.java
" trong cùng thư mực với lớp "Circle.java
" (bởi vì chúng ta đang sửa dụng lại class Circle
). Biên dịch và chạy chương trình.
2.2 Method Overriding & Variable Hiding
Một lớp con kế thừa tất cả các biến và phương thức thành viên từ các siêu lớp của nó (cha mẹ trực tiếp và tất cả tổ tiên của nó). Class con có thể sử dụng các phương thức và biến được kế thừa như là của chính nó. Nó cũng có thể ghi đè một phương thức được kế thừa bằng cách cung cấp phiên bản của chính nó hoặc ẩn một biến được kế thừa bằng cách xác định một biến có cùng tên.
Ví dụ, phương thức được kế thừa getArea()
trong một đối tượng Cylinder
sẽ tính diện tích cơ sở của hình trụ. Giả sử rằng chúng ta quyết định ghi đè lên getArea()
để tính diện tích bề mặt của hình trụ trong lớp con Cylinder
. Dưới đây là những thay đổi:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class Cylinder extends Circle {
......
@Override
public double getArea() {
return 2*Math.PI*getRadius()*height + 2*super.getArea();
}
public double getVolume() {
return super.getArea()*height;
}
@Override
public String toString() {
return "Cylinder[" + super.toString() + ",height=" + height + "]";
}
}
|
Nếu getArea()
được gọi từ một đối tượng Circle
, nó sẽ tính diện tích của vòng tròn. Nếu getArea()
được gọi từ một đối tượng Cylinder
, nó sẽ tính diện tích bề mặt của hình trụ bằng cách sử dụng thực hiện ghi đè . Lưu ý rằng bạn phải sử dụng phương pháp accessor công cộng getRadius()
để lấy radius
của Circle
, bởi vì radius
được khai báo private
và do đó không thể truy cập đến các lớp khác, kể cả các lớp con Cylinder
.
Nhưng nếu bạn ghi đè getArea()
trong Cylinder
, thì getVolume()
( =getArea()*height
) không còn hoạt động. Đó là bởi vì phần ghi đè getArea()
sẽ được sử dụng Cylinder
, không tính diện tích cơ sở. Bạn có thể khắc phục sự cố này bằng cách sử dụng super.getArea()
để sử dụng phiên bản của siêu lớp getArea()
. Lưu ý rằng super.getArea()
chỉ có thể được ban hành từ định nghĩa lớp con, nhưng không thể gọi từ một đối tượng được tạo, ví dụ c1.super.getArea()
, vì nó phá vỡ nguyên tắc ẩn và đóng gói thông tin.
2.3 Annotation @Override (JDK 1.5)
" @Override
" Được gọi là chú thích - Annotation (được giới thiệu trong JDK 1.5), yêu cầu trình biên dịch kiểm tra xem liệu có một phương thức như vậy trong siêu lớp được ghi đè hay không. Điều này giúp ích rất nhiều nếu bạn viết sai tên của phương thức được ghi đè. Ví dụ: giả sử bạn muốn ghi đè phương thức toString()
trong một lớp con. Nếu @Override
không được sử dụng và toString()
bị sai chính tả TOString()
, nó sẽ được coi là một phương thức mới trong lớp con, thay vì ghi đè lên lớp cha. Nếu @Override
được sử dụng, trình biên dịch sẽ báo hiệu lỗi.
@Override
chú thích là tùy chọn, nhưng chắc chắn rằng nếu có thì sẽ hay hơn :)
Chú thích không phải là cấu trúc lập trình. Nó không có ảnh hưởng đến đầu ra chương trình. Nó chỉ được sử dụng bởi trình biên dịch, loại bỏ sau khi biên dịch và không được sử dụng bởi bộ thực thi.
2.4 Keyword "super"
Hãy nhớ lại rằng bên trong một định nghĩa lớp, bạn có thể sử dụng từ khóa this
để chỉ trường hợp này . Tương tự, từ khóa super
đề cập đến siêu lớp, có thể là cha mẹ ngay cận kề hoặc tổ tiên của nó.
Từ khóa super
cho phép lớp con truy cập các phương thức và biến của siêu lớp trong định nghĩa của lớp con. Ví dụ, super()
và có thể được sử dụng gọi hàm tạo của lớp cha. Nếu lớp con ghi đè một phương thức được kế thừa từ siêu lớp của nó , bạn có thể sử dụng để gọi phiên bản của lớp cha trong định nghĩa của lớp con. Tương tự, nếu lớp con của bạn ẩn một trong các biến của lớp cha, bạn có thể sử dụng để tham chiếu đến biến ẩn trong định nghĩa của lớp con.super(argumentList)
getArea()
super.getArea()
super.variableName
2.5 Bàn thêm về hàm tạo - Constructors
Hãy nhớ lại rằng lớp con kế thừa tất cả các biến và phương thức từ các siêu lớp của nó. Tuy nhiên, lớp con không kế thừa các hàm tạo của các siêu lớp của nó. Mỗi lớp trong Java định nghĩa các hàm tạo riêng của nó.
Trong phần thân của hàm tạo, bạn có thể sử dụng để gọi hàm tạo của siêu lớp ngay lập tức của nó. Lưu ý rằng , nếu nó được sử dụng, phải là câu lệnh đầu tiên trong hàm tạo của lớp con. Nếu nó không được sử dụng trong hàm tạo, trình biên dịch Java sẽ tự động chèn một câu lệnh để gọi hàm tạo không có đối số của siêu lớp ngay lập tức của nó. Điều này theo sau thực tế là cha mẹ phải được sinh ra trước khi đứa trẻ có thể được sinh ra. Bạn cần xây dựng các siêu lớp đúng cách trước khi bạn có thể xây dựng lớp con.super(args)
super(args)
super()
2.6 Default no-arg Constructor
Nếu không có hàm tạo nào được định nghĩa trong một lớp, trình biên dịch Java sẽ tự động tạo một hàm tạo không có đối số (không có đối số) , chỉ đơn giản là thực hiện một lời gọi super()
, như sau:
public ClassName () {
super();
}
Hãy lưu ý rằng:
- Hàm tạo không có đối số mặc định sẽ không được tạo tự động, nếu một (hoặc nhiều) hàm tạo được xác định. Nói cách khác, bạn cần xác định rõ ràng constructor không có đối số nếu các constructor khác được định nghĩa.
- Nếu siêu lớp ngay lập tức không có hàm tạo mặc định (nó xác định một số hàm tạo nhưng không xác định hàm tạo không có đối số), bạn sẽ gặp lỗi biên dịch khi thực hiện
super()
cuộc gọi. Lưu ý rằng trình biên dịch Java chèn một super()
câu lệnh đầu tiên trong hàm tạo nếu không có super(args)
.
2.7 Single Inheritance
Java không hỗ trợ nhiều kế thừa (C ++ không). Nhiều kế thừa cho phép một lớp con có nhiều hơn một siêu lớp trực tiếp. Điều này có một nhược điểm nghiêm trọng nếu các siêu lớp có triển khai xung đột cho cùng một phương thức. Trong Java, mỗi lớp con có thể có một và chỉ một siêu lớp trực tiếp, nghĩa là thừa kế đơn. Mặt khác, một siêu lớp có thể có nhiều lớp con.
2.8 Common Root Class - java.lang.Object
Java áp dụng một cách tiếp cận cái gọi là gốc chung . Tất cả các lớp Java có nguồn gốc từ một lớp gốc chung được gọi là java.lang.Object
. Object
Lớp này định nghĩa và thực hiện các hành vi phổ biến được yêu cầu đối với tất cả các đối tượng Java đang chạy trong JRE. Những hành vi phổ biến này cho phép thực hiện các tính năng như đa luồng và trình thu gom rác.
2.9 Inheritance EG. 2: The Point2D and Point3D Classes
The Superclass Point2D.java
public class Point2D {
private int x, y;
public Point2D() {
this.x = 0;
this.y = 0;
}
public Point2D(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return this.x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return this.y;
}
public void setY(int y) {
this.y = y;
}
@Override
public String toString() {
return "(" + this.x + "," + this.y + ")";
}
}
The Subclass Point3D.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
public class Point3D extends Point2D {
private int z;
public Point3D() {
super();
this.z = 0;
}
public Point3D(int x, int y, int z) {
super(x, y);
this.z = z;
}
public int getZ() {
return this.z;
}
public void setZ(int z) {
this.z = z;
}
@Override
public String toString() {
return "(" + super.getX() + "," + super.getY() + "," + this.z + ")";
}
}
|
A Test Driver for Point2D and Point3D Classes (TestPoint2DPoint3D.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
public class TestPoint2DPoint3D {
public static void main(String[] args) {
Point2D p2a = new Point2D(1, 2);
System.out.println(p2a);
Point2D p2b = new Point2D();
System.out.println(p2b);
p2a.setX(3);
p2a.setY(4);
System.out.println(p2a);
System.out.println("x is: " + p2a.getX());
System.out.println("x is: " + p2a.getY());
Point3D p3a = new Point3D(11, 12, 13);
System.out.println(p3a);
Point2D p3b = new Point3D();
System.out.println(p3b);
p3a.setX(21);
p3a.setY(22);
p3a.setZ(23);
System.out.println(p3a);
System.out.println("x is: " + p3a.getX());
System.out.println("y is: " + p3a.getY());
System.out.println("z is: " + p3a.getZ());
}
}
|
2.10 Inheritance EG. 3: Superclass Person and its Subclasses
Giả sử rằng chúng tôi được yêu cầu làm mẫu cho học sinh và giáo viên trong ứng dụng của chúng tôi. Chúng ta có thể định nghĩa một lớp cha gọi Person
đến thuộc tính lưu trữ thông thường như name
và address
, và các lớp con Student
và Teacher
cho các thuộc tính cụ thể của họ. Đối với sinh viên, chúng tôi cần duy trì các khóa học đã thực hiện và điểm số tương ứng của họ; thêm một khóa học với lớp, in tất cả các khóa học và lớp trung bình. Giả sử rằng một sinh viên học không quá 30 khóa học cho toàn bộ chương trình. Đối với giáo viên, chúng tôi cần duy trì các khóa học hiện tại và có thể thêm hoặc xóa một khóa học được giảng dạy. Giả sử rằng một giáo viên dạy không quá 5 khóa học đồng thời.
Chúng tôi thiết kế các lớp như sau.
The Superclass Person.java
public class Person {
private String name, address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return name + "(" + address + ")";
}
}
The Subclass Student.java
public class Student extends Person {
private int numCourses;
private String[] courses;
private int[] grades;
private static final int MAX_COURSES = 30;
public Student(String name, String address) {
super(name, address);
numCourses = 0;
courses = new String[MAX_COURSES];
grades = new int[MAX_COURSES];
}
@Override
public String toString() {
return "Student: " + super.toString();
}
public void addCourseGrade(String course, int grade) {
courses[numCourses] = course;
grades[numCourses] = grade;
++numCourses;
}
public void printGrades() {
System.out.print(this);
for (int i = 0; i < numCourses; ++i) {
System.out.print(" " + courses[i] + ":" + grades[i]);
}
System.out.println();
}
public double getAverageGrade() {
int sum = 0;
for (int i = 0; i < numCourses; i++ ) {
sum += grades[i];
}
return (double)sum/numCourses;
}
}
The Subclass Teacher.java
public class Teacher extends Person {
private int numCourses;
private String[] courses;
private static final int MAX_COURSES = 5;
public Teacher(String name, String address) {
super(name, address);
numCourses = 0;
courses = new String[MAX_COURSES];
}
@Override
public String toString() {
return "Teacher: " + super.toString();
}
public boolean addCourse(String course) {
for (int i = 0; i < numCourses; i++) {
if (courses[i].equals(course)) return false;
}
courses[numCourses] = course;
numCourses++;
return true;
}
public boolean removeCourse(String course) {
boolean found = false;
int courseIndex = -1;
for (int i = 0; i < numCourses; i++) {
if (courses[i].equals(course)) {
courseIndex = i;
found = true;
break;
}
}
if (found) {
for (int i = courseIndex; i < numCourses-1; i++) {
courses[i] = courses[i+1];
}
numCourses--;
return true;
} else {
return false;
}
}
}
A Test Driver (TestPerson.java)
public class TestPerson {
public static void main(String[] args) {
Student s1 = new Student("Tan Ah Teck", "1 Happy Ave");
s1.addCourseGrade("IM101", 97);
s1.addCourseGrade("IM102", 68);
s1.printGrades();
System.out.println("Average is " + s1.getAverageGrade());
Teacher t1 = new Teacher("Paul Tan", "8 sunset way");
System.out.println(t1);
String[] courses = {"IM101", "IM102", "IM101"};
for (String course: courses) {
if (t1.addCourse(course)) {
System.out.println(course + " added");
} else {
System.out.println(course + " cannot be added");
}
}
for (String course: courses) {
if (t1.removeCourse(course)) {
System.out.println(course + " removed");
} else {
System.out.println(course + " cannot be removed");
}
}
}
}
2.11 Exercises
LINK TO EXERCISES
3. Composition vs. Inheritance
3.1 "Một dòng bao gồm 2 điểm" so với "Một dòng là một điểm được mở rộng bởi một điểm khác""
Hãy nhớ lại rằng có hai cách sử dụng lại các lớp hiện có: thành phần và kế thừa . Chúng ta đã thấy rằng một Line
lớp có thể được thực hiện bằng cách sử dụng thành phần của Point
lớp - "Một dòng bao gồm hai điểm", trong phần trước.
A Line
cũng có thể được thực hiện, sử dụng tính kế thừa từ Point
lớp - "Một dòng là một điểm được mở rộng bởi một điểm khác". Hãy gọi lớp con này LineSub
(để phân biệt với Line
lớp sử dụng thành phần).
The Superclass Point.java
như trên
The Subclass LineSub.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
public class LineSub extends Point {
Point end;
public LineSub(int x1, int y1, int x2, int y2) {
super(x1, y1);
this.end = new Point(x2, y2);
}
public LineSub(Point begin, Point end) {
super(begin.getX(), begin.getY());
this.end = end;
}
public Point getBegin() {
return this;
}
public Point getEnd() {
return end;
}
public void setBegin(Point begin) {
super.setX(begin.getX());
super.setY(begin.getY());
}
public void setEnd(Point end) {
this.end = end;
}
public int getBeginX() {
return super.getX();
}
public void setBeginX(int x) {
super.setX(x);
}
public int getBeginY() {
return super.getY();
}
public void setBeginY(int y) {
super.setY(y);
}
public int[] getBeginXY() {
return super.getXY();
}
public void setBeginXY(int x, int y) {
super.setXY(x, y);
}
public int getEndX() {
return end.getX();
}
public void setEndX(int x) {
end.setX(x);
}
public int getEndY() {
return end.getY();
}
public void setEndY(int y) {
end.setY(y);
}
public int[] getEndXY() {
return end.getXY();
}
public void setEndXY(int x, int y) {
end.setXY(x, y);
}
public String toString() {
return "LineSub[begin=" + super.toString() + ",end=" + end + "]";
}
public double getLength() {
return super.distance(end);
}
}
|
A Test Driver (TestLineSub.java)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
public class TestLineSub {
public static void main(String[] args) {
LineSub l1 = new LineSub(1, 2, 3, 4);
System.out.println(l1);
LineSub l2 = new LineSub(new Point(5,6), new Point(7,8));
System.out.println(l2);
l1.setBegin(new Point(11, 12));
l1.setEnd(new Point(13, 14));
System.out.println(l1);
System.out.println("begin is: " + l1.getBegin());
System.out.println("end is: " + l1.getEnd());
l1.setBeginX(21);
l1.setBeginY(22);
l1.setEndX(23);
l1.setEndY(24);
System.out.println(l1);
System.out.println(l1);
System.out.println("begin's x is: " + l1.getBeginX());
System.out.println("begin's y is: " + l1.getBeginY());
System.out.println("end's x is: " + l1.getEndX());
System.out.println("end's y is: " + l1.getEndY());
l1.setBeginXY(31, 32);
l1.setEndXY(33, 34);
System.out.println(l1);
System.out.println("begin's x is: " + l1.getBeginXY()[0]);
System.out.println("begin's y is: " + l1.getBeginXY()[1]);
System.out.println("end's x is: " + l1.getEndXY()[0]);
System.out.println("end's y is: " + l1.getEndXY()[1]);
System.out.printf("length is: %.2f%n", l1.getLength());
}
}
|
Lưu ý: Đây là trình điều khiển thử nghiệm tương tự được sử dụng trong ví dụ trước về thành phần, ngoại trừ thay đổi tên lớp.
Nghiên cứu cả hai phiên bản của lớp Line ( Line
và LineSub
). Tôi cho rằng sẽ dễ dàng hơn khi nói rằng "Một dòng gồm hai điểm" so với "Một dòng là một điểm được mở rộng bởi một điểm khác".
Nguyên tắc chung: Sử dụng thành phần nếu có thể, trước khi xem xét kế thừa. Chỉ sử dụng kế thừa nếu có mối quan hệ phân cấp rõ ràng giữa các lớp.
Đăng nhận xét