equals
có hashCode
liên quan chặt chẽ với nhau và nên ghi đè cả hai phương thức này trong các lớp của họ một cách nhất quán. Một số ít hơn một chút biết tại sao lại như vậy và hậu quả đáng buồn nào có thể xảy ra nếu quy tắc này bị phá vỡ. Tôi đề nghị xem xét khái niệm về các phương pháp này, lặp lại mục đích của chúng và hiểu lý do tại sao chúng có mối liên hệ với nhau như vậy. Tôi đã viết bài viết này, giống như bài trước về việc tải các lớp, cho chính mình để cuối cùng tiết lộ tất cả các chi tiết của vấn đề và không quay lại các nguồn của bên thứ ba nữa. Vì vậy, tôi sẽ rất vui khi nhận được những lời chỉ trích mang tính xây dựng, bởi vì nếu có kẽ hở ở đâu đó thì chúng cần được loại bỏ. Than ôi, bài viết hóa ra khá dài.
bằng quy tắc ghi đè
Một phương thứcequals()
được yêu cầu trong Java để xác nhận hoặc phủ nhận thực tế là hai đối tượng có cùng nguồn gốc là bằng nhau về mặt logic . Nghĩa là, khi so sánh hai đối tượng, người lập trình cần hiểu liệu các trường quan trọng của chúng có tương đương nhau hay không . Không nhất thiết tất cả các trường phải giống hệt nhau vì phương thức này equals()
hàm ý sự bằng nhau về mặt logic . Nhưng đôi khi không có nhu cầu đặc biệt để sử dụng phương pháp này. Như họ nói, cách dễ nhất để tránh các vấn đề khi sử dụng một cơ chế cụ thể là không sử dụng nó. Cũng cần lưu ý rằng một khi bạn phá vỡ hợp đồng, equals
bạn sẽ mất quyền kiểm soát việc hiểu các đối tượng và cấu trúc khác sẽ tương tác với đối tượng của bạn như thế nào. Và sau đó việc tìm ra nguyên nhân gây ra lỗi sẽ rất khó khăn.
Khi nào không ghi đè phương pháp này
- Khi mỗi thể hiện của một lớp là duy nhất. Ở mức độ lớn hơn, điều này áp dụng cho những lớp cung cấp hành vi cụ thể thay vì được thiết kế để làm việc với dữ liệu. Chẳng hạn như lớp
- Trong thực tế, lớp không bắt buộc phải xác định sự tương đương của các thể hiện của nó. Ví dụ, đối với một lớp,
- Khi lớp bạn đang mở rộng đã có cách triển khai phương thức riêng
equals
và hành vi của việc triển khai này phù hợp với bạn. Ví dụ: đối với các lớp - Và cuối cùng, không cần ghi đè
equals
khi phạm vi lớp của bạn làprivate
hoặcpackage-private
và bạn chắc chắn rằng phương thức này sẽ không bao giờ được gọi.
Thread
. Đối với họ equals
, việc thực hiện phương thức do lớp cung cấp Object
là quá đủ. Một ví dụ khác là các lớp enum ( Enum
).
java.util.Random
không cần phải so sánh các thể hiện của lớp đó với nhau, xác định xem chúng có thể trả về cùng một chuỗi số ngẫu nhiên hay không. Đơn giản vì bản chất của lớp này thậm chí không bao hàm hành vi như vậy.
Set
, List
, Map
việc triển khai tương ứng equals
là trong AbstractSet
, AbstractList
và AbstractMap
.
bằng hợp đồng
Khi ghi đè một phương thức,equals
nhà phát triển phải tuân thủ các quy tắc cơ bản được xác định trong đặc tả ngôn ngữ Java.
- tính phản xạ với bất kỳ giá trị đã cho nào
- Đối diện đối với mọi giá trị đã cho
- Tính chuyển tiếp đối với bất kỳ giá trị đã cho nào và
- Tính nhất quán đối với bất kỳ giá trị nhất định nào
- So sánh null đối với bất kỳ giá trị nhất định nào,
x
, biểu thức x.equals(x)
phải trả về true
.
Cho - có nghĩa là như vậy
x != null
x
và y
, chỉ x.equals(y)
trả về true
nếu nó y.equals(x)
trả về true
.
x
nếu trả về và trả về thì phải trả về giá trị đó . y
z
x.equals(y)
true
y.equals(z)
true
x.equals(z)
true
x
và y
lệnh gọi lặp lại x.equals(y)
sẽ trả về giá trị của lệnh gọi trước đó cho phương thức này, miễn là các trường được sử dụng để so sánh hai đối tượng không thay đổi giữa các lệnh gọi.
x
cuộc gọi x.equals(null)
phải trả về false
.
tương đương vi phạm hợp đồng
Nhiều lớp, chẳng hạn như các lớp từ Khung sưu tập Java, phụ thuộc vào việc triển khai phương thứcequals()
, vì vậy bạn không nên bỏ qua nó, bởi vì Việc vi phạm hợp đồng của phương pháp này có thể dẫn đến hoạt động không hợp lý của ứng dụng và trong trường hợp này sẽ khá khó để tìm ra lý do. Theo nguyên lý phản thân , mọi vật đều phải tương đương với chính nó. Nếu nguyên tắc này bị vi phạm, khi chúng ta thêm một đối tượng vào bộ sưu tập rồi tìm kiếm nó bằng phương thức, contains()
chúng ta sẽ không thể tìm thấy đối tượng mà chúng ta vừa thêm vào bộ sưu tập. Điều kiện đối xứng phát biểu rằng hai đối tượng bất kỳ phải bằng nhau bất kể thứ tự chúng được so sánh. Ví dụ: nếu bạn có một lớp chỉ chứa một trường kiểu chuỗi, sẽ không chính xác khi so sánh equals
trường này với một chuỗi trong một phương thức. Bởi vì trong trường hợp so sánh ngược, phương thức sẽ luôn trả về giá trị false
.
// Нарушение симметричности
public class SomeStringify {
private String s;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof SomeStringify) {
return s.equals(((SomeStringify) o).s);
}
// нарушение симметричности, классы разного происхождения
if (o instanceof String) {
return s.equals(o);
}
return false;
}
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
if (this == o) return true;
return o instanceof SomeStringify &&
((SomeStringify) o).s.equals(s);
}
Từ điều kiện bắc cầu, suy ra rằng nếu hai trong ba đối tượng bất kỳ bằng nhau thì trong trường hợp này cả ba đối tượng đều phải bằng nhau. Nguyên tắc này có thể dễ dàng bị vi phạm khi cần mở rộng một lớp cơ sở nào đó bằng cách thêm một thành phần có ý nghĩa vào nó . Ví dụ: đối với một lớp Point
có tọa độ x
và y
bạn cần thêm màu của điểm bằng cách mở rộng nó. Để làm điều này, bạn sẽ cần khai báo một lớp ColorPoint
với trường tương ứng color
. Do đó, nếu trong lớp mở rộng chúng ta gọi equals
phương thức cha, còn trong lớp cha chúng ta giả sử chỉ có tọa độ x
và được so sánh y
thì hai điểm có màu khác nhau nhưng có cùng tọa độ sẽ được coi là bằng nhau, điều này không chính xác. Trong trường hợp này, cần dạy lớp dẫn xuất cách phân biệt màu sắc. Để làm điều này, bạn có thể sử dụng hai phương pháp. Nhưng người ta sẽ vi phạm quy luật đối xứng và điều thứ hai - tính bắc cầu .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
Trong trường hợp này, lệnh gọi point.equals(colorPoint)
sẽ trả về giá trị true
và phép so sánh colorPoint.equals(point)
sẽ trả về false
, bởi vì mong đợi một đối tượng thuộc lớp “của nó”. Như vậy, quy luật đối xứng bị vi phạm. Phương pháp thứ hai liên quan đến việc thực hiện kiểm tra "mù" trong trường hợp không có dữ liệu về màu của điểm, tức là chúng ta có lớp Point
. Hoặc kiểm tra màu nếu có thông tin về nó, tức là so sánh một đối tượng của lớp ColorPoint
.
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
// Слепая проверка
if (!(o instanceof ColorPoint))
return super.equals(o);
// Полная проверка, включая цвет точки
return super.equals(o) && ((ColorPoint) o).color == color;
}
Nguyên tắc bắc cầu bị vi phạm ở đây như sau. Giả sử có một định nghĩa về các đối tượng sau:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
Như vậy, mặc dù đẳng thức p1.equals(p2)
và được thỏa mãn p2.equals(p3)
nhưng p1.equals(p3)
nó sẽ trả về giá trị false
. Đồng thời, theo tôi, phương pháp thứ hai có vẻ kém hấp dẫn hơn, bởi vì Trong một số trường hợp, thuật toán có thể bị che khuất và không thực hiện so sánh đầy đủ và bạn có thể không biết về nó. Một chút thơ Nói chung, theo tôi hiểu, không có giải pháp cụ thể nào cho vấn đề này. Có ý kiến từ một tác giả có thẩm quyền tên là Kay Horstmann rằng bạn có thể thay thế việc sử dụng toán tử instanceof
bằng một lệnh gọi phương thức getClass()
trả về lớp của đối tượng và trước khi bạn bắt đầu so sánh chính các đối tượng đó, hãy đảm bảo rằng chúng cùng loại , và không chú ý đến thực tế nguồn gốc chung của chúng. Như vậy, các quy luật đối xứng và tính bắc cầu sẽ được thỏa mãn. Nhưng đồng thời, ở phía bên kia rào cản là một tác giả khác, không kém phần được kính trọng trong giới rộng rãi, Joshua Bloch, người tin rằng cách tiếp cận này vi phạm nguyên tắc thay thế của Barbara Liskov. Nguyên tắc này nêu rõ rằng “mã gọi phải xử lý lớp cơ sở giống như các lớp con của nó mà không biết . ” Và trong giải pháp do Horstmann đề xuất, nguyên tắc này rõ ràng đã bị vi phạm, vì nó phụ thuộc vào việc thực hiện. Tóm lại rõ ràng là sự việc đen tối. Cũng cần lưu ý rằng Horstmann làm rõ quy tắc áp dụng phương pháp của mình và viết bằng tiếng Anh đơn giản rằng bạn cần quyết định chiến lược khi thiết kế các lớp và nếu việc kiểm tra tính bằng nhau chỉ được thực hiện bởi siêu lớp, bạn có thể thực hiện điều này bằng cách thực hiện hoạt động instanceof
. Mặt khác, khi ngữ nghĩa của việc kiểm tra thay đổi tùy thuộc vào lớp dẫn xuất và việc triển khai phương thức cần được chuyển xuống cấp bậc thấp hơn, bạn phải sử dụng phương thức này getClass()
. Ngược lại, Joshua Bloch đề xuất từ bỏ tính kế thừa và sử dụng thành phần đối tượng bằng cách đưa một ColorPoint
lớp vào lớp đó Point
và cung cấp một phương thức truy cập asPoint()
để lấy thông tin cụ thể về điểm. Điều này sẽ tránh vi phạm tất cả các quy tắc, nhưng theo tôi, nó sẽ làm cho mã khó hiểu hơn. Tùy chọn thứ ba là sử dụng tính năng tự động tạo phương thức bằng bằng IDE. Nhân tiện, ý tưởng tái tạo thế hệ Horstmann, cho phép bạn chọn chiến lược triển khai một phương thức trong siêu lớp hoặc trong các hậu duệ của nó. Cuối cùng, quy tắc nhất quán tiếp theo nêu rõ rằng ngay cả khi các đối tượng x
không y
thay đổi, việc gọi lại chúng x.equals(y)
phải trả về cùng giá trị như trước. Quy tắc cuối cùng là không có đối tượng nào bằng null
. Mọi thứ đều rõ ràng ở đây null
- đây là sự không chắc chắn, đối tượng có bằng sự không chắc chắn không? Nó không rõ ràng, tức là false
.
Thuật toán chung để xác định bằng
- Kiểm tra sự bằng nhau của các tham chiếu đối tượng
this
và các tham số phương thứco
.if (this == o) return true;
- Kiểm tra xem liên kết có được xác định hay không
o
, tức là có đúng khôngnull
.
Nếu trong tương lai, khi so sánh các loại đối tượng, toán tử sẽ được sử dụnginstanceof
thì mục này có thể bị bỏ qua vì tham số này trả vềfalse
trong trường hợp nàynull instanceof Object
. - So sánh các loại đối tượng
this
bằng cách sử dụngo
toán tửinstanceof
hoặc phương thứcgetClass()
, được hướng dẫn bởi mô tả ở trên và trực giác của riêng bạn. - Nếu một phương thức
equals
bị ghi đè trong một lớp con, hãy nhớ thực hiện cuộc gọisuper.equals(o)
- Chuyển đổi loại tham số
o
thành lớp được yêu cầu. - Thực hiện so sánh tất cả các trường đối tượng quan trọng:
- đối với các kiểu nguyên thủy (ngoại trừ
float
vàdouble
), sử dụng toán tử==
- đối với các trường tham chiếu, bạn cần gọi phương thức của chúng
equals
- đối với mảng, bạn có thể sử dụng phép lặp tuần hoàn hoặc phương thức
Arrays.equals()
- cho các loại
float
vàdouble
cần sử dụng các phương pháp so sánh của các lớp bao bọc tương ứngFloat.compare()
vàDouble.compare()
- đối với các kiểu nguyên thủy (ngoại trừ
- Và cuối cùng, hãy trả lời ba câu hỏi: phương pháp được triển khai có đối xứng không ? Bắc cầu ? Đã đồng ý ? Hai nguyên tắc còn lại ( tính phản xạ và tính chắc chắn ) thường được thực hiện một cách tự động.
Quy tắc ghi đè HashCode
Hàm băm là một số được tạo từ một đối tượng mô tả trạng thái của nó tại một thời điểm nào đó. Số này được sử dụng chủ yếu trong Java trong các bảng băm nhưHashMap
. Trong trường hợp này, hàm băm để lấy một số dựa trên một đối tượng phải được triển khai theo cách đảm bảo sự phân bố tương đối đồng đều các phần tử trên bảng băm. Và cũng để giảm thiểu khả năng xung đột khi hàm trả về cùng một giá trị cho các khóa khác nhau.
Mã băm hợp đồng
Để triển khai hàm băm, đặc tả ngôn ngữ xác định các quy tắc sau:- việc gọi một phương thức
hashCode
một hoặc nhiều lần trên cùng một đối tượng phải trả về cùng một giá trị băm, miễn là các trường liên quan đến việc tính giá trị của đối tượng không thay đổi. - việc gọi một phương thức
hashCode
trên hai đối tượng phải luôn trả về cùng một số nếu các đối tượng bằng nhau (gọi một phương thứcequals
trên các đối tượng này trả vềtrue
). - gọi một phương thức
hashCode
trên hai đối tượng không bằng nhau phải trả về các giá trị băm khác nhau. Mặc dù yêu cầu này không bắt buộc nhưng cần lưu ý rằng việc thực hiện nó sẽ có tác động tích cực đến hiệu suất của bảng băm.
Các phương thức bằng và hashCode phải được ghi đè cùng nhau
Dựa trên các hợp đồng được mô tả ở trên, theo đó, khi ghi đè phương thức trong mã của bạnequals
, bạn phải luôn ghi đè phương thức đó hashCode
. Vì trên thực tế, hai phiên bản của một lớp là khác nhau vì chúng nằm trong các vùng bộ nhớ khác nhau nên chúng phải được so sánh theo một số tiêu chí logic. Theo đó, hai đối tượng tương đương về mặt logic phải trả về cùng một giá trị băm. Điều gì xảy ra nếu chỉ một trong những phương pháp này bị ghi đè?
-
equals
cóhashCode
khôngGiả sử chúng ta đã xác định chính xác một phương thức
equals
trong lớp của mình vàhashCode
quyết định giữ nguyên phương thức đó trong lớpObject
. Khi đó, theo quan điểm của phương thức,equals
hai đối tượng sẽ bằng nhau về mặt logic, trong khi theo quan điểm của phương thức,hashCode
chúng sẽ không có điểm chung nào. Và do đó, bằng cách đặt một đối tượng vào bảng băm, chúng ta có nguy cơ không lấy lại được nó bằng khóa.
Ví dụ như thế này:Map<Point, String> m = new HashMap<>(); m.put(new Point(1, 1), “Point A”); // pointName == null String pointName = m.get(new Point(1, 1));
Rõ ràng, đối tượng được đặt và đối tượng được tìm kiếm là hai đối tượng khác nhau, mặc dù về mặt logic chúng bằng nhau. Nhưng bởi vì chúng có các giá trị băm khác nhau vì chúng tôi đã vi phạm hợp đồng, chúng tôi có thể nói rằng chúng tôi đã đánh mất đối tượng của mình ở đâu đó trong lòng bảng băm.
-
hashCode
cóequals
không.Điều gì sẽ xảy ra nếu chúng ta ghi đè phương thức
hashCode
vàequals
kế thừa việc triển khai phương thức đó từ lớpObject
. Như bạn đã biết,equals
phương thức mặc định chỉ đơn giản so sánh các con trỏ với các đối tượng, xác định xem chúng có tham chiếu đến cùng một đối tượng hay không. Giả sử rằnghashCode
chúng ta đã viết phương thức theo tất cả các quy tắc, cụ thể là đã tạo nó bằng IDE và nó sẽ trả về cùng một giá trị băm cho các đối tượng giống hệt nhau về mặt logic. Rõ ràng, bằng cách làm như vậy chúng ta đã xác định được một số cơ chế để so sánh hai đối tượng.Do đó, về mặt lý thuyết, ví dụ ở đoạn trước sẽ được thực hiện. Nhưng chúng ta vẫn không thể tìm thấy đối tượng của mình trong bảng băm. Mặc dù chúng ta sẽ tiến gần đến điều này, bởi vì ở mức tối thiểu chúng ta sẽ tìm thấy một giỏ bảng băm trong đó đối tượng sẽ nằm.
Để tìm kiếm thành công một đối tượng trong bảng băm, ngoài việc so sánh các giá trị băm của khóa, việc xác định sự bằng nhau logic của khóa với đối tượng tìm kiếm còn được sử dụng. Nghĩa là,
equals
không có cách nào để thực hiện mà không ghi đè phương thức.
Thuật toán chung xác định hashCode
Ở đây, đối với tôi, bạn không nên lo lắng quá nhiều và hãy tạo phương thức trong IDE yêu thích của mình. Bởi vì tất cả những sự dịch chuyển bit này sang phải và trái để tìm kiếm tỷ lệ vàng, tức là phân phối chuẩn - điều này dành cho những anh chàng hoàn toàn cứng đầu. Cá nhân tôi nghi ngờ rằng mình có thể làm tốt hơn và nhanh hơn cùng một Ý tưởng.Thay vì một kết luận
Vì vậy, chúng ta thấy rằng các phương thứcequals
đóng hashCode
một vai trò được xác định rõ ràng trong ngôn ngữ Java và được thiết kế để đạt được đặc tính bình đẳng logic của hai đối tượng. Trong trường hợp của phương pháp, equals
điều này có liên quan trực tiếp đến việc so sánh các đối tượng, trong trường hợp hashCode
gián tiếp, khi cần thiết, giả sử, để xác định vị trí gần đúng của một đối tượng trong bảng băm hoặc cấu trúc dữ liệu tương tự để tăng tốc độ tìm kiếm đối tượng. Ngoài hợp đồng , equals
còn hashCode
có một yêu cầu khác liên quan đến việc so sánh các đối tượng. Đây là tính nhất quán của một phương thức compareTo
giao diện Comparable
với equals
. Yêu cầu này bắt buộc nhà phát triển phải luôn quay lại x.equals(y) == true
khi x.compareTo(y) == 0
. Nghĩa là, chúng ta thấy rằng việc so sánh logic giữa hai đối tượng không được mâu thuẫn ở bất kỳ đâu trong ứng dụng và phải luôn nhất quán.
GO TO FULL VERSION