JavaRush /Blog Java /Random-VI /Hợp đồng bằng và hashCode hoặc bất cứ thứ gì
Aleksandr Zimin
Mức độ
Санкт-Петербург

Hợp đồng bằng và hashCode hoặc bất cứ thứ gì

Xuất bản trong nhóm
Tất nhiên, đại đa số các lập trình viên Java biết rằng các phương thức equalshashCodeliê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ức equals()đượ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, equalsbạ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 Thread. Đối với họ equals, việc thực hiện phương thức do lớp cung cấp Objectlà quá đủ. Một ví dụ khác là các lớp enum ( Enum).
  • 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, java.util.Randomkhô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.
  • Khi lớp bạn đang mở rộng đã có cách triển khai phương thức riêng equalsvà 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 Set, List, Mapviệc triển khai tương ứng equalslà trong AbstractSet, AbstractListAbstractMap.
  • Và cuối cùng, không cần ghi đè equalskhi phạm vi lớp của bạn là privatehoặc package-privatevà bạn chắc chắn rằng phương thức này sẽ không bao giờ được gọi.

bằng hợp đồng

Khi ghi đè một phương thức, equalsnhà 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 x, biểu thức x.equals(x)phải trả về true.
    Cho - có nghĩa là như vậyx != null
  • Đối diện
  • đối với mọi giá trị đã cho xy, chỉ x.equals(y)trả về truenếu nó y.equals(x)trả về true.
  • Tính chuyển tiếp
  • đối với bất kỳ giá trị đã cho nào và xnếu trả về và trả về thì phải trả về giá trị đó . yzx.equals(y)truey.equals(z)truex.equals(z)true
  • Tính nhất quán
  • đối với bất kỳ giá trị nhất định nào xylệ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.
  • So sánh null
  • đối với bất kỳ giá trị nhất định nào, xcuộ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ức equals(), 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 equalstrườ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 Pointcó tọa độ xybạ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 ColorPointvới trường tương ứng color. Do đó, nếu trong lớp mở rộng chúng ta gọi equalsphương thức cha, còn trong lớp cha chúng ta giả sử chỉ có tọa độ xvà được so sánh ythì 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ị truevà 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ử instanceofbằ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ứngtí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 ColorPointlớp vào lớp đó Pointvà 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 xkhông ythay đổ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

  1. Kiểm tra sự bằng nhau của các tham chiếu đối tượng thisvà các tham số phương thức o.
    if (this == o) return true;
  2. Kiểm tra xem liên kết có được xác định hay không o, tức là có đúng không null.
    Nếu trong tương lai, khi so sánh các loại đối tượng, toán tử sẽ được sử dụng instanceofthì mục này có thể bị bỏ qua vì tham số này trả về falsetrong trường hợp này null instanceof Object.
  3. So sánh các loại đối tượng thisbằng cách sử dụng otoán tử instanceofhoặc phương thức getClass(), được hướng dẫn bởi mô tả ở trên và trực giác của riêng bạn.
  4. Nếu một phương thức equalsbị ghi đè trong một lớp con, hãy nhớ thực hiện cuộc gọisuper.equals(o)
  5. Chuyển đổi loại tham số othành lớp được yêu cầu.
  6. 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ừ floatdouble), 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úngequals
    • đố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ứcArrays.equals()
    • cho các loại floatdoublecần sử dụng các phương pháp so sánh của các lớp bao bọc tương ứng Float.compare()Double.compare()
  7. 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ạ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 hashCodemộ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 hashCodetrê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ức equalstrên các đối tượng này trả về true).
  • gọi một phương thức hashCodetrê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ạn equals, 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 đè?
  1. equalshashCodekhông

    Giả sử chúng ta đã xác định chính xác một phương thức equalstrong lớp của mình và hashCodequyết định giữ nguyên phương thức đó trong lớp Object. Khi đó, theo quan điểm của phương thức, equalshai đối tượng sẽ bằng nhau về mặt logic, trong khi theo quan điểm của phương thức, hashCodechú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.

  2. hashCodeequalskhông.

    Điều gì sẽ xảy ra nếu chúng ta ghi đè phương thức hashCodeequalskế thừa việc triển khai phương thức đó từ lớp Object. Như bạn đã biết, equalsphươ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ằng hashCodechú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à, equalskhô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ức equalsđóng hashCodemộ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 hashCodegiá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 , equalscòn hashCodecó 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 compareTogiao diện Comparablevớ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) == truekhi 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.

Nguồn

Java hiệu quả, Phiên bản thứ hai Joshua Bloch. Dịch miễn phí một cuốn sách rất hay. Java, thư viện của chuyên gia. Tập 1. Cơ bản. Kay Horstmann. Ít lý thuyết hơn một chút và thực hành nhiều hơn. Nhưng mọi thứ không được phân tích chi tiết như của Bloch. Mặc dù có một quan điểm giống nhau bằng(). Cấu trúc dữ liệu trong hình ảnh HashMap Một bài viết cực kỳ hữu ích về thiết bị HashMap trong Java. Thay vì nhìn vào nguồn.
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION