What are the implications of not implementing equals() and hashcode() in entity classes?

Hi all,

so in our system, we had OneToMany relationships as Lists, and ManyToManys as Sets.

But then we encountered the dreaded duplicates issue (For ex, How to Avoid Duplicate Records in Hibernate/Spring Data JPA). The OneToMany lists contained multiple references to instances with the same hash code. So we changed the OneToManys to sets, and the issue was resolved. All was well.

While debugging and researching the issue, however, I came across this article (and others saying similar things): How to implement equals and hashCode using the JPA entity identifier (Primary Key) - Vlad Mihalcea

It made sense. In a data-centric system, of course you’d want to consider instances with the same db identifier, living in the same tables, as equal. So then we implemented that.

But here’s the rub: our system doesn’t just heavily fetch and write data from and to the DB, it also does a lot of in-memory processing. Now that whenever we put entities in a set, or as keys in a map, an equals() method that contains a bit of logic had to be executed (since hashcode(), based on Vlad’s recommendation above, just returned the hashcode of the class), this method ended up being invoked many thousands of times a second. And that adversely affected performance, in some cases seriously so.

Best practices are best practices, but not if they cost an arm and a leg performance-wise. Everything seems to work just fine without overriding hashcode() and equals().

What exactly are the implications of failing to override these two? Thanks in advance!

You will only notice a problem when your hashCode is based on a generated identifier, yet try to add an entity with a null identifier to a hash based set/map, which is later persisted and hence the identifier set.
In such a case, the object would “change” its hash code, which is what is problematic.
The effects of that are that calling contains/remove for sets or containsKey/remove and get for maps will return false/null, because the newly passed object/key will have a hashCode for which there is most likely no entry with a matching hash code in the underlying hash table.

You have more or less 3 ways to solve this:

  • Don’t use sets/maps based on mutable state (you have the same problem with trees when implementing Comparable.compareTo based on a generated primary key), but rather use lists
  • Don’t add elements to sets/maps before you called persist on them. Can be easily force by throwing an exception in the hashCode()/compareTo() method if the identifier is null
  • Use a random UUID for equals/hashCode which you assign when constructing the object making it always non-null and safe to use

Failing to override equals/hashCode should usually pose no problem, unless you are trying to use a detached object to find an element in a collection e.g.

entity.getSet().contains(new MyEntity(1L));

Such a check will always fail, because the default equals/hashCode semantics are based on object identity. Though that is easily solved, just ask the persistence context for the managed object:

entity.getSet().contains(entityManager.getReference(MyEntity.class, 1L));
1 Like

Thank you for the thorough answer, it makes perfect sense.