JavaRush /Java Blog /Random EN /equals & hashCode Methods: Usage Practice

equals & hashCode Methods: Usage Practice

Published in the Random EN group
Hello! Today we will talk about two important methods in Java - equals()and hashCode(). This is not the first time we meet with them: at the beginning of the CodeGym course there was a short lecture about equals()- read it if you forgot or didn’t meet it before. equals & methods  hashCode: usage practice - 1In today's lesson, we will talk about these concepts in detail - believe me, there is something to talk about! And before moving on to the new, let's refresh our memory of what we have already covered :) As you remember, the usual comparison of two objects through the “ ” operator ==is a bad idea, because “ ==” compares references. Here is our example with cars from a recent lecture:
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);
   }
}
Console output:

false
It would seem that we have created two identical class objects Car: all the fields of the two machines are the same, but the result of the comparison is still false. We already know the reason: the references car1and car2point to different addresses in memory, so they are not equal. We still want to compare two objects, not two references. The best solution for comparing objects is the equals().

equals() method

You may remember that we do not create this method from scratch, but override it - after all, the method equals()is defined in the Object. However, in its usual form, it is of little use:
public boolean equals(Object obj) {
   return (this == obj);
}
This is how the method equals()is defined in the class Object. Same link comparison. Why was it made like this? Well, how do the creators of the language know which objects in your program are considered equal and which are not? :) This is the main idea of ​​the method equals()- the creator of the class himself determines the characteristics by which the equality of objects of this class is checked. By doing this, you override the method equals()in your class. If you do not quite understand the meaning of "you define the characteristics yourself", let's look at an example. Here is a simple person class - 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.
}
Let's say we're writing a program that needs to determine if two people are twin relatives or if they're just doppelgangers. We have five characteristics: nose size, eye color, hairstyle, scars, and the results of a biological DNA test (for simplicity, in the form of a code number). Which of these characteristics do you think will allow our program to identify twin relatives? equals & methods  hashCode: usage practice - 2Of course, only a biological test can give a guarantee. Two people can have the same eye color, hairstyle, nose, and even scars - there are many people in the world, and it is impossible to avoid coincidences. We need a reliable mechanism: only the result of a DNA test allows us to draw an accurate conclusion. What does this mean for our method equals()? We need to override it in the classManaccording to the requirements of our program. The method must compare the field int dnaCodeof two objects, and if they are equal, then the objects are equal.
@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Is it really that simple? Not really. We missed something. In this case, for our objects, we have defined only one “significant” field, according to which their equality is established - dnaCode. Now imagine that we would have not 1, but 50 such "significant" fields. And if all 50 fields of two objects are equal, then the objects are equal. This can also be. The main problem is that calculating the equality of 50 fields is a time-consuming and resource-consuming process. Now imagine that in addition to the class, Manwe have a class Womanwith exactly the same fields as in Man. And if another programmer will use your classes, he can easily write something like this in his program:
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));
}
Checking the field values ​​in this case is pointless: we see that we have objects of two different classes, and they cannot be equal in principle! So equals()we need to put a check in the method - a comparison of objects of two identical classes. It's good that we thought about it!
@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
But maybe we forgot something else? Hmm... At a minimum, we should check that we are not comparing the object with itself! If references A and B point to the same address in memory, then this is the same object, and we also do not need to spend time comparing 50 fields.
@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;
}
Also, it doesn't hurt to add a check for null: no object can be equal to null, in which case there is no point in additional checks. With all this in mind, our equals()class method Manwill look like this:
@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;
}
We carry out all the initial checks mentioned above. If in the end it turned out that:
  • we are comparing two objects of the same class
  • it is not the same object
  • we are comparing our object not cnull
...then we move on to comparing significant characteristics. In our case, the fields dnaCodeof two objects. When overriding a method equals(), be sure to follow these requirements:
  1. Reflexivity.

    Any object must be equals()itself.
    We have already taken into account this requirement. Our method says:

    if (this == o) return true;

  2. Symmetry.

    If a.equals(b) == true, then , and b.equals(a)should return true.
    Our method also meets this requirement.

  3. Transitivity.

    If two objects are equal to some third object, then they must be equal to each other.
    If a.equals(b) == trueand a.equals(c) == true, then the test b.equals(c)must also return true.

  4. Constancy.

    The results of the work equals()should change only when the fields included in it change. If the data of the two objects has not changed, the results of checking against equals()should always be the same.

  5. Inequality with null.

    For any object, the check a.equals(null)must return false.
    This is not just a set of some “useful recommendations”, but a rigid method contract prescribed in the Oracle documentation

equals & methods  hashCode: usage practice - 3

hashcode() method

Now let's talk about the method hashCode(). Why is it needed? Exactly for the same purpose - comparisons of objects. But we already have equals()! Why another method? The answer is simple: to improve performance. The hash function, which is represented in Java by the method hashCode(), returns a fixed-length numeric value for any object. In the case of Java, the method hashCode()returns for any object a 32-bit number of type int. Comparing two numbers with each other is much faster than comparing two objects with the method equals(), especially if it uses a lot of fields. If objects are compared in our program, it is much easier to do this by hash code, and only if they are equal in hashCode()- go to comparison byequals(). By the way, this is how hash-based data structures work - for example, the one you know HashMap! The method hashCode(), like the method equals(), is overridden by the developer himself. And just like for , there are official requirements equals()for the method , written in the Oracle documentation:hashCode()
  1. If two objects are equal (i.e. the method equals()returns true), they must have the same hash code.

    Otherwise, our methods will be meaningless. Checking for hashCode(), as we said, should go first to improve performance. If the hash codes are different, the check will return false, even though the objects are actually equal (as per our definition in the method equals()).

  2. If a method hashCode()is called multiple times on the same object, it must return the same number each time.

  3. Rule 1 doesn't work the other way around. Two different objects can have the same hash code.

The third rule is a bit confusing. How can this be? The explanation is quite simple. The method hashCode()returns int. intis a 32-bit number. It has a limited number of values ​​- from -2,147,483,648 to +2,147,483,647. In other words, there are just over 4 billion variants of the number int. Now imagine that you are creating a program to store data about all living people on Earth. Each person will have their own class object Man. ~7.5 billion people live on earth. In other words, no matter how good the object transformation algorithmManwe didn’t write in the number, we simply don’t have enough numbers. We only have 4.5 billion options, and there are many more people. So, no matter how hard we try, for some different people the hash codes will be the same. This situation (coincidence of hash codes for two different objects) is called a collision. One of the tasks of a programmer when overriding a method hashCode()is to reduce the potential number of collisions as much as possible. How will our method hashCode()for the class look like Man, taking into account all these rules? Like this:
@Override
public int hashCode() {
   return dnaCode;
}
Surprised? :) Unexpectedly, but if you look at the requirements, you will see that we comply with everything. Objects for which ours equals()returns true will be equal to hashCode(). If our two objects Manare equal in equals(that is, they have the same dnaCode), our method will return the same number. Let's consider a more complicated example. Let's say our program is to select luxury cars for collector clients. Collecting is a complex thing, and it has many features. A 1963 car can cost 100 times more than the same car from 1964. A red car from 1970 can cost 100 times more than a blue car of the same brand from the same year. equals & methods  hashCode: usage practice - 4In the first case, with the classMan, we discarded most of the fields (i.e., human characteristics) as insignificant and used only the field for comparison dnaCode. Here we are working with a very peculiar sphere, and there can be no minor details! Here is our class 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.
}
Here, when comparing, we must take into account all fields. Any mistake can cost hundreds of thousands of dollars to the client, so it's best to play it safe:
@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);
}
In our method, equals()we have not forgotten about all the checks that we talked about earlier. But now we are comparing each of the three fields of our objects. In this program, equality must be absolute, for each field. But what about hashCode?
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
The field modelin our class is a string. This is convenient: Stringthe method hashCode()has already been overridden in the class. We calculate the hash code of the field model, and add the sum of the other two numeric fields to it. Java has a little trick that is used to reduce the number of collisions: when calculating the hash code, multiply the intermediate result by an odd prime number. The most commonly used number is 29 or 31. We won't get into the math now, but for future reference, multiplying intermediate results by a large enough odd number helps spread the results of the hash function and end up with fewer objects with the same hashcode. For our method hashCode()in LuxuryAuto, it would look like this:
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
You can read more about all the intricacies of this mechanism in this post on StackOverflow , as well as in Joshua Bloch's book Effective Java . Finally, there is one more important point worth mentioning. Each time when redefining equals()and hashCode()we chose certain fields of the object, which were taken into account in these methods. But can we account for different fields in equals()and hashCode()? Technically, we can. But this is a bad idea, and here's why:
@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;
}
Here are our methods for the LuxuryAuto class equals()as well hashCode(). The method hashCode()remained unchanged, and equals()we removed the model. Now the model is not a characteristic for comparing two objects by equals(). But it is still taken into account when calculating the hash code. What will we get as a result? Let's create two cars and check it out!
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
Error! By using different fields for equals()and hashCode()we violated the contract established for them! Two equal equals()objects must have the same hash code. We have obtained different values ​​for them. Mistakes like this can lead to the most incredible consequences, especially when working with collections that use a hash. Therefore, when redefining equals()and, hashCode()it will be correct to use the same fields. The lecture turned out to be rather big, but today you learned a lot of new things! :) It's time to get back to solving problems!
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION