Декабрь 7, 2025
Demystifying Java’s equals() and hashCode()
The equals and hashCode methods are two fundamental methods in Java that every single java developer must know in detail.There are tons of books and articles that explain the purpose of those methods in detail. I had been interviewing java developer recently; but interestingly, many of them struggled to answer some of the simple questions related to equals hash code contract violations. Therefore, decided to touch this topic which is already discussed over and over. Here, I will use some concrete code examples. Let’s begin.
1.Suppose we are given the following Java class, what is the violation here? Consider that the equals method is a valid one.
import java.util.Objects;
public class Student {
private Long id;
private String name;
// getters and setters here
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(id, student.id);
}
}
At first look, it seems like everything is fine here, right? But it is not, remember the equals hashCode contract. Always override hashCode if you override equals. More on this could be found in the book “Effective Java, Methods Common to All Objects”. If we don’t override hashCode, there is no guaranty that two equals objects will have the same hashCode, unless they refer to the same object. In this case, it is possible that we will put data in a hash data structure such as HashMap or HashSet but we will not be able to find it later. E.g.
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Set<User> set = new HashSet<>();
User user1 = new User(1L);
User user2 = new User(1L);
user1.equals(user2); //always true, same user
set.add(user1); //added user1 instance
System.out.println(set.contains(user2)); //looking with user2 instance
// May return false, but should be true always
}
}
import java.util.Objects;
public class User {
private Long id;
public User(Long id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
}
2. Let’s imagine we have the following class, what is violated here?
import java.util.Objects;
public class Student {
private Long id;
private String name;
// getters and setters here
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(id, student.id);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
Now, if we look at the class it seems everything is fine right? We have overridden equals, and we also have overridden hashCode. So, seems like everything is right. Unfortunately, now we have broken another rule. If two objects are equal, then their hashCode should be the same. In the example above, if two objects have the same id, they are treated as equal but if the names are not the same, then the hashCodes will be different.
3. Is it valid hashCode implementation?
@Override
public int hashCode() {
return 1;
}
The short answer to this is, it is absolutely yes. Remember that if two objects are equals then their hashCode must be the same, but that does not require that un-equal objects can’t have the same hashCode. This case is called hash collision, where different (un-equal) objects produce the same hashCode. Many hashCode implementations may result in hash collusion, certainly we should try to avoid it but it does not necessarily mean we are violating any contract requirements.
4. What is the consequence of the hashCode implementation given in the previous example?
Although we are not violating equals hashCode contract, we should avoid overriding hashCode in this way as there is a price to pay. If we read the working internals of a HashMap for example, we can say that we need an even distribution of the elements, to improve the search time. This implementation, will put all elements in the a single bucket, and the elements will be chained as a linked list. The cost of this is, we will now need O(n) time to find an element. However, in Java 8, there is a huge improvement and the elements are no longer chained as a linked list but as a binary search tree, after a certain threshold is reached. That means the worst search time will be O(log(n)).
5. What is wrong with the following equals implementation?
import java.util.Objects;
public class Animal {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Animal(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof Animal) {
Animal animal = (Animal) o;
return Objects.equals(id, animal.id);
}
return false;
}
}
Initially, it seems ok but if we pay attention to the Animal class, it is not final and that means we can create a child of it. Now, let’s suppose we have created a Dog subclass that extends Animal, and overridden the equals method again.
package data.structures.trees.test;
import java.util.Objects;
public class Animal {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Animal(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof Animal) {
Animal animal = (Animal) o;
return Objects.equals(id, animal.id);
}
return false;
}
}
class Dog extends Animal {
public Dog(Long id, String name) {
super(id, name);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof Dog) {
Dog dog = (Dog) o;
return Objects.equals(getId(), dog.getId());
}
return false;
}
public static void main(String[] args) {
Animal animal = new Animal(1L, "Dog");
Dog dog = new Dog(1L, "Dog");
System.out.println(dog.equals(animal)); //false
System.out.println(animal.equals(dog)); //true
}
}
Now, if you run the code above, dog.equals(animal) will return false, but animal.equals(dog) will return true. Remember that, equals should be symmetric, so if a.equals(b) then b.equals(a) should also be true. Therefore, a class type check is a good idea. E.g. if (o == null || getClass() != o.getClass()) return false;
6. Finally, what is wrong with the below implementation of the equals method which includes a class type check as well?
import java.util.Objects;
public class Animal {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Animal(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Animal animal = (Animal) o;
return Objects.equals(id, animal.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
The implementation here is autogenerated by IDE, and probably one of the most commonly used equals implementation with less problems. If you say this is perfectly valid equals implementation, well you are kind of right but there is also some discussion around the class type check. Well, the type check actually breaks Liskov substitution principle from SOLID principles. So how? Let’s see another example. Suppose we have a Point(x,y) class, and a ColoredPoint(x,y,color) class. Now, let’s implement the equals method.
import java.awt.Color;
import java.util.Objects;
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
class ColoredPoint extends Point {
private Color color;
public ColoredPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
public static void main(String[] args) {
Point point = new Point(1, 1);
ColoredPoint point1 = new ColoredPoint(1, 1, Color.RED);
System.out.println(point.equals(point1)); //false
}
}
Logically, point and point1 are referring to the same point with x cord 1, and y cord 1. However, ColoredPoint is holding some additional metadata about that point. Now, should point.equals(point1) return false or true? :)
I hope you find the examples useful, and hope you success in you next interview!
Готовы начать учиться?
Смотреть карьерные пути →