CodeGym /Blog Java /Ngẫu nhiên /phương pháp bằng và mã băm: thực tiễn tốt nhất

phương pháp bằng và mã băm: thực tiễn tốt nhất

Xuất bản trong nhóm
CHÀO! Hôm nay chúng ta sẽ nói về hai phương thức quan trọng trong Java: equals()hashCode(). Đây không phải là lần đầu tiên chúng tôi gặp họ: khóa học CodeGym bắt đầu bằng một bài học ngắn về equals()— hãy đọc nếu bạn quên hoặc chưa từng xem... phương pháp bằng và mã băm: thực tiễn tốt nhất - 1Trong bài học hôm nay, chúng ta sẽ nói về các khái niệm này một cách chi tiết. Và tin tôi đi, chúng ta có chuyện để nói đấy! Nhưng trước khi chúng ta chuyển sang cái mới, hãy làm mới những gì chúng ta đã đề cập :) Như bạn nhớ, việc so sánh hai đối tượng bằng toán tử thường là một ý tưởng tồi ==, bởi vì ==so sánh các tham chiếu. Đây là ví dụ của chúng tôi với ô tô từ một bài học gần đây:

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
Đầu ra bảng điều khiển:

false
Có vẻ như chúng ta đã tạo hai Carđối tượng giống hệt nhau: giá trị của các trường tương ứng của hai đối tượng ô tô là như nhau, nhưng kết quả so sánh vẫn sai. Chúng ta đã biết lý do: tham chiếu car1car2trỏ đến các địa chỉ bộ nhớ khác nhau, vì vậy chúng không bằng nhau. Nhưng chúng tôi vẫn muốn so sánh hai đối tượng, không phải hai tham chiếu. Giải pháp tốt nhất để so sánh các đối tượng là equals()phương thức.

phương thức bằng ()

Bạn có thể nhớ rằng chúng tôi không tạo phương thức này từ đầu, thay vào đó chúng tôi ghi đè lên nó: phương equals()thức được định nghĩa trong Objectlớp. Điều đó nói rằng, ở dạng thông thường, nó ít được sử dụng:

public boolean equals(Object obj) {
   return (this == obj);
}
Đây là cách equals()phương thức được định nghĩa trong Objectlớp. Đây là một so sánh các tài liệu tham khảo một lần nữa. Tại sao họ làm cho nó như vậy? Chà, làm thế nào để những người tạo ngôn ngữ biết đối tượng nào trong chương trình của bạn được coi là bình đẳng và đối tượng nào không? :) Đây là điểm chính của equals()phương pháp — người tạo ra một lớp là người xác định những đặc điểm nào được sử dụng khi kiểm tra sự bằng nhau của các đối tượng trong lớp. Sau đó, bạn ghi đè equals()phương thức trong lớp của mình. Nếu bạn không hiểu lắm ý nghĩa của "xác định đặc điểm nào", hãy xem xét một ví dụ. Đây là một lớp đơn giản đại diện cho một người đàn ông: Man.

public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   // Getters, setters, etc.
}
Giả sử chúng ta đang viết một chương trình cần xác định xem hai người là song sinh giống hệt nhau hay chỉ đơn giản là giống nhau. Chúng tôi có năm đặc điểm: kích thước mũi, màu mắt, kiểu tóc, sự hiện diện của các vết sẹo và kết quả xét nghiệm DNA (để đơn giản, chúng tôi biểu thị điều này dưới dạng mã số nguyên). Bạn nghĩ đặc điểm nào sau đây sẽ cho phép chương trình của chúng ta xác định các cặp song sinh giống hệt nhau? phương pháp bằng và mã băm: thực tiễn tốt nhất - 2Tất nhiên, chỉ có xét nghiệm DNA mới có thể đảm bảo. Hai người có thể có màu mắt, kiểu tóc, mũi và thậm chí cả vết sẹo giống nhau — có rất nhiều người trên thế giới và không thể đảm bảo rằng không có bất kỳ người doppelgänger nào ngoài kia. Nhưng chúng tôi cần một cơ chế đáng tin cậy: chỉ có kết quả xét nghiệm ADN mới cho phép chúng tôi đưa ra kết luận chính xác. Điều này có ý nghĩa gì đối với equals()phương pháp của chúng tôi? Chúng ta cần ghi đè lên nó trongManclass, có tính đến các yêu cầu của chương trình của chúng tôi. Phương thức nên so sánh int dnaCodetrường của hai đối tượng. Nếu chúng bằng nhau thì các đối tượng bằng nhau.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Là nó thực sự là đơn giản? Không thực sự. Chúng tôi đã bỏ qua một cái gì đó. Đối với các đối tượng của chúng tôi, chúng tôi chỉ xác định một trường có liên quan đến việc thiết lập sự bình đẳng đối tượng: dnaCode. Bây giờ hãy tưởng tượng rằng chúng ta không chỉ có 1 mà là 50 trường liên quan. Và nếu tất cả 50 trường của hai đối tượng bằng nhau, thì các đối tượng bằng nhau. Một kịch bản như vậy cũng có thể xảy ra. Vấn đề chính là việc thiết lập sự bình đẳng bằng cách so sánh 50 trường là một quá trình tốn nhiều thời gian và nguồn lực. Bây giờ hãy tưởng tượng rằng ngoài Manlớp của chúng ta, chúng ta còn có một Womanlớp với các trường giống hệt nhau tồn tại trong Man. Nếu một lập trình viên khác sử dụng các lớp của chúng tôi, họ có thể dễ dàng viết mã như sau:

public static void main(String[] args) {
  
   Man man = new Man(........); // A bunch of parameters in the constructor

   Woman woman = new Woman(.........); // The same bunch of parameters.

   System.out.println(man.equals(woman));
}
Trong trường hợp này, việc kiểm tra các giá trị trường là vô nghĩa: chúng ta có thể dễ dàng thấy rằng chúng ta có các đối tượng thuộc hai lớp khác nhau, vì vậy không thể nào chúng bằng nhau được! Điều này có nghĩa là chúng ta nên thêm một kiểm tra vào equals()phương thức, so sánh các lớp của các đối tượng được so sánh. Thật tốt khi chúng tôi nghĩ về điều đó!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Nhưng có lẽ chúng ta đã quên một cái gì đó khác? Hmm... Ở mức tối thiểu, chúng ta nên kiểm tra xem chúng ta có đang so sánh một đối tượng với chính nó không! Nếu các tham chiếu A và B trỏ đến cùng một địa chỉ bộ nhớ, thì chúng là cùng một đối tượng và chúng ta không cần lãng phí thời gian để so sánh 50 trường.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Cũng không hại gì khi thêm một kiểm tra cho null: không có đối tượng nào có thể bằng null. Vì vậy, nếu tham số phương thức là null, thì không cần kiểm tra thêm. Với tất cả những điều này, thì equals()phương thức của chúng ta cho Manlớp sẽ như sau:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Chúng tôi thực hiện tất cả các kiểm tra ban đầu được đề cập ở trên. Vào cuối ngày, nếu:
  • chúng ta đang so sánh hai đối tượng của cùng một lớp
  • và các đối tượng được so sánh không phải là cùng một đối tượng
  • và đối tượng được truyền không phải lànull
...sau đó chúng tôi tiến hành so sánh các đặc điểm có liên quan. Đối với chúng tôi, điều này có nghĩa là dnaCodecác trường của hai đối tượng. Khi ghi đè equals()phương thức, hãy đảm bảo tuân thủ các yêu cầu sau:
  1. phản xạ.

    Khi equals()phương thức được sử dụng để so sánh bất kỳ đối tượng nào với chính nó, nó phải trả về true.
    Chúng tôi đã tuân thủ yêu cầu này. Phương pháp của chúng tôi bao gồm:

    
    if (this == o) return true;
    

  2. Đối diện.

    Nếu a.equals(b) == true, sau đó b.equals(a)phải trở lại true.
    Phương pháp của chúng tôi cũng đáp ứng yêu cầu này.

  3. Tính chuyển tiếp.

    Nếu hai đối tượng bằng một số đối tượng thứ ba, thì chúng phải bằng nhau.
    Nếu a.equals(b) == truea.equals(c) == true, thì b.equals(c)cũng phải trả về true.

  4. kiên trì.

    Kết quả của equals()must thay đổi chỉ khi các trường liên quan được thay đổi. Nếu dữ liệu của hai đối tượng không thay đổi thì kết quả của equals()phải luôn giống nhau.

  5. Bất đẳng thức với null.

    Đối với bất kỳ đối tượng nào, a.equals(null)phải trả về false
    Đây không chỉ là một tập hợp một số "khuyến nghị hữu ích", mà là một hợp đồng nghiêm ngặt , được quy định trong tài liệu của Oracle

phương thức hashCode()

Bây giờ hãy nói về hashCode()phương pháp. Tại sao nó cần thiết? Đối với cùng một mục đích - để so sánh các đối tượng. Nhưng chúng tôi đã có equals()! Tại sao một phương pháp khác? Câu trả lời rất đơn giản: để cải thiện hiệu suất. Một hàm băm, được biểu diễn trong Java bằng hashCode()phương thức, trả về một giá trị số có độ dài cố định cho bất kỳ đối tượng nào. Trong Java, hashCode()phương thức trả về một số 32 bit ( int) cho bất kỳ đối tượng nào. So sánh hai số nhanh hơn nhiều so với so sánh hai đối tượng bằng equals()phương pháp, đặc biệt nếu phương pháp đó xem xét nhiều trường. Nếu chương trình của chúng ta so sánh các đối tượng, thì việc sử dụng mã băm sẽ đơn giản hơn nhiều. Chỉ khi các đối tượng bằng nhau dựa trên hashCode()phương thức thì việc so sánh mới tiến hànhequals()phương pháp. Nhân tiện, đây là cách hoạt động của các cấu trúc dữ liệu dựa trên hàm băm, chẳng hạn như HashMap! Phương hashCode()thức, giống như equals()phương thức, được ghi đè bởi nhà phát triển. Và cũng giống như equals(), hashCode()phương thức này có các yêu cầu chính thức được nêu rõ trong tài liệu của Oracle:
  1. Nếu hai đối tượng bằng nhau (nghĩa là equals()phương thức trả về true), thì chúng phải có cùng mã băm.

    Nếu không, phương pháp của chúng tôi sẽ là vô nghĩa. Như chúng tôi đã đề cập ở trên, hashCode()nên kiểm tra trước để cải thiện hiệu suất. Nếu các mã băm khác nhau, thì kiểm tra sẽ trả về giá trị sai, mặc dù các đối tượng thực sự bằng nhau theo cách chúng ta đã xác định phương equals()thức.

  2. Nếu hashCode()phương thức được gọi nhiều lần trên cùng một đối tượng, thì mỗi lần nó phải trả về cùng một số.

  3. Quy tắc 1 không hoạt động theo hướng ngược lại. Hai đối tượng khác nhau có thể có cùng mã băm.

Quy tắc thứ ba là một chút khó hiểu. Làm sao có thể? Lời giải thích khá đơn giản. Phương thức này hashCode()trả về một tệp int. An intlà một số 32 bit. Nó có phạm vi giá trị giới hạn: từ -2.147.483.648 đến +2.147.483.647. Nói cách khác, chỉ có hơn 4 tỷ giá trị có thể có cho một tên miền int. Bây giờ, hãy tưởng tượng rằng bạn đang tạo một chương trình để lưu trữ dữ liệu về tất cả những người sống trên Trái đất. Mỗi người sẽ tương ứng với đối tượng riêng Person(tương tự như lớp Man). Có ~ 7,5 tỷ người sống trên hành tinh. Nói cách khác, cho dù thuật toán chúng ta viết để chuyển đổi thông minh đến đâuPersonđối tượng thành một int, chúng tôi chỉ đơn giản là không có đủ số lượng có thể. Chúng tôi chỉ có 4,5 tỷ giá trị int có thể, nhưng có rất nhiều người hơn thế. Điều này có nghĩa là bất kể chúng ta cố gắng thế nào, một số người khác nhau sẽ có cùng mã băm. Khi điều này xảy ra (mã băm trùng với hai đối tượng khác nhau), chúng tôi gọi đó là xung đột. Khi ghi đè hashCode()phương thức, một trong những mục tiêu của lập trình viên là giảm thiểu số lượng xung đột tiềm ẩn. Kế toán cho tất cả các quy tắc này, hashCode()phương thức sẽ như thế nào trong Personlớp? Như thế này:

@Override
public int hashCode() {
   return dnaCode;
}
Ngạc nhiên? :) Nếu bạn nhìn vào các yêu cầu, bạn sẽ thấy rằng chúng tôi tuân thủ tất cả. Các đối tượng mà equals()phương thức của chúng ta trả về true cũng sẽ bằng nhau theo hashCode(). Nếu hai Personđối tượng của chúng ta bằng nhau equals(nghĩa là chúng có cùng dnaCode), thì phương thức của chúng ta trả về cùng một số. Hãy xem xét một ví dụ khó khăn hơn. Giả sử chương trình của chúng ta nên chọn những chiếc xe hơi sang trọng cho những người sưu tập xe hơi. Sưu tập có thể là một sở thích phức tạp với nhiều đặc thù. Một chiếc xe cụ thể năm 1963 có thể đắt gấp 100 lần so với một chiếc xe năm 1964. Một chiếc ô tô màu đỏ đời 1970 có thể đắt gấp 100 lần một chiếc ô tô màu xanh cùng nhãn hiệu sản xuất cùng năm. phương pháp bằng và mã băm: thực tiễn tốt nhất - 4Trong ví dụ trước của chúng tôi, với Personlớp, chúng tôi đã loại bỏ hầu hết các trường (tức là đặc điểm của con người) là không đáng kể và chỉ sử dụng trườngdnaCodetrường trong so sánh. Chúng tôi hiện đang làm việc trong một lĩnh vực rất đặc trưng, ​​trong đó không có chi tiết nào không quan trọng! Đây là LuxuryAutolớp học của chúng tôi:

public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   // ...getters, setters, etc.
}
Bây giờ chúng ta phải xem xét tất cả các lĩnh vực trong so sánh của chúng ta. Bất kỳ sai lầm nào cũng có thể khiến khách hàng mất hàng trăm nghìn đô la, vì vậy sẽ tốt hơn nếu quá an toàn:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
Theo equals()phương pháp của chúng tôi, chúng tôi đã không quên tất cả các kiểm tra mà chúng tôi đã nói trước đó. Nhưng bây giờ chúng ta so sánh từng trường trong số ba trường của các đối tượng của chúng ta. Đối với chương trình này, chúng tôi cần sự bình đẳng tuyệt đối, tức là bình đẳng trong từng lĩnh vực. Thế còn hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Trường modeltrong lớp của chúng tôi là một Chuỗi. Điều này thuận tiện vì Stringlớp đã ghi đè hashCode()phương thức. Chúng tôi tính toán modelmã băm của trường và sau đó cộng tổng của hai trường số khác vào đó. Các nhà phát triển Java có một thủ thuật đơn giản mà họ sử dụng để giảm số lần va chạm: khi tính toán một mã băm, hãy nhân kết quả trung gian với một số nguyên tố lẻ. Số được sử dụng phổ biến nhất là 29 hoặc 31. Chúng tôi sẽ không đi sâu vào các chi tiết toán học ngay bây giờ, nhưng trong tương lai, hãy nhớ rằng việc nhân các kết quả trung gian với một số lẻ đủ lớn sẽ giúp "trải rộng" kết quả của hàm băm và, do đó, giảm số lượng đối tượng có cùng mã băm. Đối với hashCode()phương pháp của chúng tôi trong LuxuryAuto, nó sẽ như thế này:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Bạn có thể đọc thêm về tất cả những điều phức tạp của cơ chế này trong bài đăng này trên StackOverflow , cũng như trong cuốn sách Java hiệu quả của Joshua Bloch. Cuối cùng, một điểm quan trọng hơn đáng được đề cập. Mỗi lần chúng tôi ghi đè phương thức equals()hashCode(), chúng tôi đã chọn một số trường mẫu nhất định được tính đến trong các phương thức này. Các phương pháp này xem xét các trường giống nhau. Nhưng liệu chúng ta có thể xem xét các trường khác nhau trong equals()hashCode()? Về mặt kỹ thuật, chúng tôi có thể. Nhưng đây là một ý tưởng tồi, và đây là lý do:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Dưới đây là các phương thức equals()và của chúng tôi hashCode()cho LuxuryAutolớp. Phương hashCode()thức vẫn không thay đổi, nhưng chúng tôi đã xóa modeltrường khỏi equals()phương thức. Mô hình không còn là một đặc tính được sử dụng khi equals()phương thức so sánh hai đối tượng. Nhưng khi tính toán mã băm, trường đó vẫn được tính đến. Kết quả là chúng ta nhận được gì? Hãy tạo hai chiếc xe và tìm hiểu!

public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
Lỗi! Bằng cách sử dụng các trường khác nhau cho các phương thức equals()hashCode(), chúng tôi đã vi phạm các hợp đồng đã được thiết lập cho chúng! Hai đối tượng bằng nhau theo equals()phương thức phải có cùng mã băm. Chúng tôi đã nhận được các giá trị khác nhau cho họ. Những lỗi như vậy có thể dẫn đến những hậu quả hoàn toàn không thể tin được, đặc biệt là khi làm việc với các bộ sưu tập sử dụng hàm băm. Do đó, khi bạn ghi đè equals()hashCode(), bạn nên xem xét các trường giống nhau. Bài học này khá dài, nhưng bạn đã học được rất nhiều ngày hôm nay! :) Bây giờ là lúc để quay lại giải quyết các nhiệm vụ!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION