Câu trả lời của Kilian Foth là tuyệt vời. Tôi chỉ muốn thêm ví dụ kinh điển * về lý do tại sao đây là một vấn đề. Hãy tưởng tượng một lớp điểm nguyên:
class Point2D {
public int x;
public int y;
// constructor
public Point2D(int theX, int theY) { x = theX; y = theY; }
public int hashCode() { return x + y; }
public boolean equals(Object o) {
if (this == o) { return true; }
if ( !(o instanceof Point2D) ) { return false; }
Point2D that = (Point2D) o;
return (x == that.x) &&
(y == that.y);
}
}
Bây giờ hãy để lớp con trở thành một điểm 3D.
class Point3D extends Point2D {
public int z;
// constructor
public Point3D(int theX, int theY, int theZ) {
super(x, y); z = theZ;
}
public int hashCode() { return super.hashCode() + z; }
public boolean equals(Object o) {
if (this == o) { return true; }
if ( !(o instanceof Point3D) ) { return false; }
Point3D that = (Point3D) o;
return super.equals(that) &&
(z == that.z);
}
}
Siêu đơn giản! Hãy sử dụng điểm của chúng tôi:
Point2D p2a = new Point2D(3, 5);
Point2D p2b = new Point2D(3, 5);
Point2D p2c = new Point2D(3, 7);
p2a.equals(p2b); // true
p2b.equals(p2a); // true
p2a.equals(p2c); // false
Point3D p3a = new Point3D(3, 5, 7);
Point3D p3b = new Point3D(3, 5, 7);
Point3D p3c = new Point3D(3, 7, 11);
p3a.equals(p3b); // true
p3b.equals(p3a); // true
p3a.equals(p3c); // false
Có lẽ bạn đang tự hỏi tại sao tôi đăng một ví dụ dễ dàng như vậy. Đây là cái bẫy:
p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!
Khi chúng ta so sánh điểm 2D với điểm 3D tương đương, chúng ta sẽ đúng, nhưng khi chúng ta đảo ngược sự so sánh, chúng ta sẽ sai (vì p2a thất bại instanceof Point3D
).
Phần kết luận
Thông thường có thể thực hiện một phương thức trong một lớp con theo cách mà nó không còn tương thích với cách mà siêu lớp mong đợi nó hoạt động.
Nhìn chung, không thể thực hiện bằng () trên một lớp con khác nhau đáng kể theo cách tương thích với lớp cha của nó.
Khi bạn viết một lớp mà bạn dự định cho phép mọi người phân lớp, đó là một ý tưởng thực sự tốt để viết ra một hợp đồng về cách mỗi phương thức nên hành xử. Thậm chí tốt hơn sẽ là một tập hợp các bài kiểm tra đơn vị mà mọi người có thể chạy chống lại việc thực hiện các phương pháp bị ghi đè của họ để chứng minh rằng họ không vi phạm hợp đồng. Hầu như không ai làm điều đó bởi vì nó quá nhiều công việc. Nhưng nếu bạn quan tâm, đó là điều cần làm.
Một ví dụ tuyệt vời của một hợp đồng được đánh vần tốt là So sánh . Chỉ cần bỏ qua những gì nó nói về .equals()
những lý do được mô tả ở trên. Đây là một ví dụ về cách so sánh có thể làm những điều .equals()
không thể .
Ghi chú
Mục 8 "Java hiệu quả" của Josh Bloch là nguồn gốc của ví dụ này, nhưng Bloch sử dụng ColorPoint để thêm màu thay vì trục thứ ba và sử dụng gấp đôi thay vì ints. Ví dụ Java của Bloch về cơ bản được sao chép bởi Oderky / Spoon / Venners , những người đã đưa ra ví dụ của họ trực tuyến.
Một số người đã phản đối ví dụ này bởi vì nếu bạn cho lớp cha biết về lớp con, bạn có thể khắc phục vấn đề này. Điều đó đúng nếu có một số lượng nhỏ các lớp con và nếu phụ huynh biết về tất cả chúng. Nhưng câu hỏi ban đầu là về việc tạo một API mà người khác sẽ viết các lớp con cho. Trong trường hợp đó, bạn thường không thể cập nhật triển khai cha mẹ để tương thích với các lớp con.
Tiền thưởng
Trình so sánh cũng thú vị bởi vì nó hoạt động xung quanh vấn đề thực hiện bằng () một cách chính xác. Tốt hơn nữa, nó tuân theo một mẫu để khắc phục loại vấn đề thừa kế này: mẫu thiết kế Chiến lược. Các kiểu chữ mà người Haskell và Scala thích thú cũng là mẫu Chiến lược. Kế thừa không phải là xấu hay sai, nó chỉ là khó khăn. Để đọc thêm, hãy xem bài viết của Philip Wadler Cách làm cho đa hình ad-hoc bớt ad hoc