JavaRush /Blog Java /Random-VI /Phương thức bằng & hashCode: thực hành sử dụng

Phương thức bằng & hashCode: thực hành sử dụng

Xuất bản trong nhóm
Xin 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ọ: ở đầu khóa học JavaRush có một bài giảng ngắn về equals()- hãy đọc nó nếu bạn quên hoặc chưa xem trước đó. Phương thức bằng &  hashCode: thực hành sử dụng - 1Trong bài học hôm nay chúng ta sẽ nói chi tiết về những khái niệm này - tin tôi đi, có rất nhiều điều để nói! Và trước khi chuyển sang điều gì đó mới, hãy ôn lại trí nhớ của chúng ta về những gì chúng ta đã đề cập :) Như bạn nhớ, việc so sánh thông thường giữa hai đối tượng bằng toán tử “ ==” 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ề ô tô từ một bài giảng 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 của bảng điều khiển:

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

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 mà ghi đè lên nó - xét cho cùng, phương thức này equals()được định nghĩa trong lớp Object. Tuy nhiên, ở dạng thông thường nó ít được sử dụng:
public boolean equals(Object obj) {
   return (this == obj);
}
Đây là cách phương thức equals()được định nghĩa trong lớp Object. Sự so sánh tương tự của các liên kết. Tại sao anh lại bị làm như thế này? Chà, làm thế nào để người tạo ra ngôn ngữ biết đối tượng nào trong chương trình của bạn được coi là bằng nhau và đối tượng nào không? :) Đây là ý tưởng chính của phương thức equals()- chính người tạo ra lớp xác định các đặc điểm để kiểm tra sự bình đẳng của các đối tượng của lớp này. Bằng cách này, bạn ghi đè phương thức equals()trong lớp của mình. Nếu bạn không hiểu rõ ý nghĩa của việc “bạn tự xác định các đặc điểm”, hãy xem một ví dụ. Đây là một lớp người đơn giản - 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 có quan hệ họ hàng với nhau là anh em sinh đôi hay chỉ là doppelgängers. 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 vết sẹo và kết quả xét nghiệm sinh học DNA (để đơn giản - ở dạng mã số). Bạn nghĩ đặc điểm nào trong số này sẽ cho phép chương trình của chúng ta xác định được họ hàng sinh đôi? Phương thức bằng &  hashCode: thực hành sử dụng - 2Tất nhiên, chỉ có xét nghiệm sinh học mới có thể đảm bảo. Hai người có thể có màu mắt, kiểu tóc, mũi giống nhau và thậm chí cả vết sẹo - trên thế giới có rất nhiều người và không thể tránh khỏi sự trùng hợp. Chúng ta cần một cơ chế đáng tin cậy: chỉ có kết quả xét nghiệm ADN mới có thể đưa ra kết luận chính xác. Điều này có ý nghĩa gì đối với phương pháp của chúng tôi equals()? Chúng ta cần định nghĩa lại nó trong một lớp Mancó tính đến các yêu cầu của chương trình. Phương thức phải so sánh trường int dnaCodecủa hai đối tượng và 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ỏ lỡ điều gì đó. Trong trường hợp này, đố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ó ý nghĩa” để thiết lập đẳng thức của chúng - dnaCode. Bây giờ hãy tưởng tượng rằng chúng ta sẽ không có 1 mà là 50 trường "có ý nghĩa" như vậy. Và nếu tất cả 50 trường của hai đối tượng đều bằng nhau thì các đối tượng đó bằng nhau. Điều này cũng có thể xảy ra. Vấn đề chính là việc tính toán sự bằng nhau của 50 trường là một quá trình tốn thời gian và tốn tài nguyên. Bây giờ hãy tưởng tượng rằng ngoài lớp, Manchúng ta còn có một lớp Womancó các trường giống hệt như trong Man. Và nếu một lập trình viên khác sử dụng các lớp của bạn, anh ta có thể dễ dàng viết vào chương trình của mình những nội dung như:
public static void main(String[] args) {

   Man man = new Man(........); //a bunch of parameters in the constructor

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

   System.out.println(man.equals(woman));
}
Trong trường hợp này, không có ích gì khi kiểm tra các giá trị trường: chúng ta thấy rằng chúng ta đang xem xét các đối tượng của hai lớp khác nhau và về nguyên tắc chúng không thể bằng nhau! Điều này có nghĩa là chúng ta cần kiểm tra phương thức equals()— so sánh các đối tượng của hai lớp giống hệt nhau. Thật tốt khi chúng tôi đã nghĩ đến điều này!
@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 điều gì khác? Hmm... Tối thiểu, chúng ta nên kiểm tra xem chúng ta có đang so sánh đối tượng với chính nó không! Nếu tham chiếu A và B trỏ đến cùng một địa chỉ trong bộ nhớ thì chúng là cùng một đối tượng và chúng ta cũng 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;
}
Ngoài ra, sẽ không hại gì nếu thêm một kiểm tra cho null: không đối tượng nào có thể bằng null, trong trường hợp đó việc kiểm tra bổ sung sẽ không có ý nghĩa gì. Nếu tính đến tất cả những điều này, phương thức equals()lớp của chúng ta Mansẽ như thế này:
@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. Nếu nó chỉ ra rằng:
  • chúng ta so sánh hai đối tượng cùng lớp
  • đây không phải là cùng một đối tượng
  • chúng tôi không so sánh đối tượng của chúng tôi vớinull
...sau đó chúng ta chuyển sang so sánh các đặc điểm quan trọng. Trong trường hợp của chúng tôi, các trường dnaCodecủa hai đối tượng. Khi ghi đè một phương thức equals(), hãy đảm bảo tuân thủ các yêu cầu sau:
  1. Tính phản xạ.

    Bất kỳ đối tượng nào cũng phải là equals()của chính nó.
    Chúng tôi đã tính đến yêu cầu này. Phương pháp của chúng tôi nêu rõ:

    if (this == o) return true;

  2. Đối diện.

    Nếu a.equals(b) == true, thì b.equals(a)nó sẽ trả về true.
    Phương pháp của chúng tôi cũng đáp ứng được yêu cầu này.

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

    Nếu hai đối tượng bằng một đối tượng thứ ba nào đó thì chúng phải bằng nhau.
    Nếu a.equals(b) == truea.equals(c) == true, thì việc kiểm tra b.equals(c)cũng sẽ trả về true.

  4. Thường trực.

    Kết quả của công việc equals()chỉ thay đổi khi các trường trong đó thay đổi. Nếu dữ liệu của hai đối tượng không thay đổi thì kết quả kiểm tra equals()sẽ luôn giống nhau.

  5. Bất bình đẳng với null.

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

phương thức hashCode()

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

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

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

  3. Quy tắc 1 không hoạt độ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 hơi khó hiểu. Làm sao có thể? Lời giải thích khá đơn giản. Phương thức hashCode()trả về int. intlà số 32 bit. Nó có số lượng 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ỷ biến thể của con số này 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ẽ có đối tượng lớp riêng của mình Man. ~ 7,5 tỷ người sống trên trái đất. Nói cách khác, cho dù Manchúng ta có viết thuật toán chuyển đổi đối tượng thành số tốt đến đâu đi chăng nữa thì chúng ta cũng sẽ không có đủ số. Chúng ta chỉ có 4,5 tỷ lựa chọn và nhiều người hơn nữa. Điều này có nghĩa là dù chúng ta có cố gắng thế nào thì mã băm vẫn sẽ giống nhau đối với một số người khác nhau. Tình huống này (mã băm của hai đối tượng khác nhau khớp với nhau) được gọi là xung đột. Một trong những mục tiêu của lập trình viên khi ghi đè một phương thức hashCode()là giảm số lượng xung đột tiềm ẩn càng nhiều càng tốt. Phương thức của chúng ta hashCode()dành cho lớp sẽ như thế nào Mankhi tính đến tất cả các quy tắc này? Như thế này:
@Override
public int hashCode() {
   return dnaCode;
}
Ngạc nhiên? :) Thật bất ngờ, nhưng nếu nhìn vào yêu cầu, bạn sẽ thấy chúng tôi tuân thủ mọi thứ. Các đối tượng mà giá trị của chúng tôi equals()trả về true sẽ bằng nhau trong hashCode(). Nếu hai đối tượng của chúng ta Mancó giá trị bằng nhau equals(nghĩa là chúng có cùng giá trị dnaCode), phương thức của chúng ta sẽ trả về cùng một số. Hãy xem xét một ví dụ phức tạp hơn. Giả sử chương trình của chúng ta nên chọn những chiếc xe sang trọng cho khách hàng sưu tập. Thu thập là một việc phức tạp và có nhiều tính năng. Một chiếc ô tô từ năm 1963 có thể đắt hơn 100 lần so với chiếc ô tô tương tự từ năm 1964. Một chiếc ô tô màu đỏ từ năm 1970 có thể đắt hơn 100 lần so với một chiếc ô tô màu xanh cùng loại sản xuất cùng năm. Phương thức bằng &  hashCode: thực hành sử dụng - 4Trong trường hợp đầu tiên, với lớp Man, chúng tôi đã loại bỏ hầu hết các trường (tức là đặc điểm con người) là không đáng kể và chỉ sử dụng trường để so sánh dnaCode. Ở đây chúng tôi đang làm việc với một khu vực rất độc đáo và không thể có những chi tiết nhỏ! Đây là lớp học của chúng tôi LuxuryAuto:
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.
}
Ở đây, khi so sánh, chúng ta phải tính đến tất cả các lĩnh vực. Bất kỳ sai sót nào cũng có thể khiến khách hàng tốn hàng trăm nghìn đô la, vì vậy tốt hơn hết là bạn nên giữ 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);
}
Trong phương pháp của chúng tôi, equals()chúng tôi không quên tất cả các bướ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 đối tượng. Trong chương trình này, sự bình đẳng phải tuyệt đối, trong mọi 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 thật tiện lợi: Stringphương thức này hashCode()đã được ghi đè trong lớp. Chúng tôi tính toán mã băm của trường modelvà cộng tổng của hai trường số còn lại vào đó. Có một mẹo nhỏ trong Java được sử dụng để giảm số lần xung đột: khi tính 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 ta sẽ không đi sâu vào chi tiết phép tính ngay bây giờ, nhưng để tham khảo sau này, 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 hoạt động và kết thúc với ít đối tượng hơn có cùng mã băm. Đối với phương pháp của chúng tôi hashCode()trong LuxuryAuto, nó sẽ trông 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, có một điểm quan trọng hơn đáng nói. Mỗi lần ghi đè equals(), hashCode()chúng tôi đã chọn một số trường nhất định của đối tượng, những trường này đã được tính đến trong các phương thức này. Nhưng liệu chúng ta có thể tính đến các lĩnh vực khác nhau trong equals()and 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 của chúng tôi equals()dành cho hashCode()lớp LuxuryAuto. Phương thức hashCode()vẫn không thay đổi và equals()chúng tôi đã xóa trường khỏi phương thức model. Bây giờ mô hình không phải là đặc điểm để so sánh hai đối tượng bằng equals(). Nhưng nó vẫn được tính đến khi tính toán mã băm. Kết quả là chúng ta sẽ nhận được gì? Hãy tạo ra hai chiếc xe và kiểm tra nó!
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());
   }
}

Эти два an object равны друг другу?
true
Какие у них хэш-codeы?
-1372326051
1668702472
Lỗi! Bằng cách sử dụng các lĩnh vực khác nhau equals()hashCode()chúng tôi đã vi phạm hợp đồng được thiết lập cho họ! Hai equals()đối tượng bằng nhau phải có cùng mã băm. Chúng tôi có ý nghĩa khác nhau cho họ. Những lỗi như vậy có thể dẫn đến những hậu quả khó lường nhất, đặ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. Vì vậy, khi xác định lại equals()hashCode()sử dụng các trường giống nhau sẽ đúng. Bài giảng hóa ra khá dài nhưng hôm nay các bạn đã học được rất nhiều điều mới! :) Đã đến lúc quay lại giải quyết vấn đề!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION