Home > Enterprise >  Using PK (id) field when overriding equals() and hashCode() methods?
Using PK (id) field when overriding equals() and hashCode() methods?

Time:07-06

I am really too confused with the equals() and hashCode() methods after reading lots of documentation and articles. Mainly, there are different kind of examples and usages that makes me too confused.

So, could you clarify me about the following points?

1. If there is not any unique field in an entity (except from id field) then should we use getClass() method or only id field in the equals() method as shown below?

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   
   // code omitted
}

2. If there is a unique key e.g. private String isbn;, then should we use only this field? Or should we combine it with getClass() as shown below?

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Book book = (Book) o;
   return isbn == book.isbn;
}

3. What about NaturalId? As far as I understood, it is used for unique fields e.g. private String isbn;. What is the purpose of its usage? Is it related to equals() and hashCode() methods?

CodePudding user response:

It all boils down to what your class actually represents, what is its identity and when should the JVM consider two objects as actually the same. The context in which the class is used determines its behavior (in this case - equality to another object).

By default Java considers two given objects "the same" only if they are actually the same instance of a class (comparison using ==). While it makes sense in case of strictly technical verification, Java applications are usually used to represent a business domain, where multiple objects may be constructed, but they should still be considered the same. An example of that could be a book (as in your question). But what does it mean that a book is the same as another?

See - it depends.

When you ask someone if they read a certain book, you give them a title and the author, they try to "match" it agains the books they've read and see if any of them is equal to criteria you provided. So equals in this case would be checking if the title and the author of a given book is the same as the other. Simple.

Now imagine that you're a Tolkien fan. If you were Polish (like me), you could have multiple "Lord of the Rings" translations available to read, but (as a fan) you would know about some translators that went a bit too far and you would like to avoid them. The title and the author is not enough, you're looking for a book with a certain ISBN identifier that will let you find a certain edition of the book. Since ISBN also contains information about the title and the author, it's not required to use them in the equals method in this case.

The third (and final) book-related example is related to a library. Both situations described above could easily happen at a library, but from the librarian point of view books are also another thing: an "item". Each book in the library (it's just an assumption, I've never worked with such a system) has it's own identifier, which can be completely separate from the ISBN (but could also be an ISBN plus something extra). When you return a book in the library it's the library identifier that matters and it should be used in this case.

To sum up: a Book as an abstraction does not have a single "equality definition". It depends on the context. Let's say we create such a hierarchy of classes:

  • Book
  • BookEdition extends Book
  • BookItem extends BookEdition
  • BookOrder extends BookEdition (not yet in the library)

Book and BookEdition are more of a value object, while BookItem and BookOrder are entities. Value objects are represented only by their values and even though they do not have an identifier, they can be equal to other ones. Entities on the other hand can include values or can even consist of value objects (e.g. BookItem could contain a BookEdition field next to its libraryId field), but they have an identifier which defines whether they are the same as another (even if their values change). Books are not a good example here (unless we imagine reassigning a library identifier to another book), but a user that changed their username is still the same user - identified by their ID.


You're not explicitly asking about that in your question, but using instanceof and getClass() can be similarly unclear. A rule of thumb would be: use getClass() as it helps to avoid problems with symmetry.

CodePudding user response:

getClass()

In regard to the usage of getClass() everything is straightforward.

Method equals() expects an argument of type Object.

It's important to ensure that you're dialing with an instance of the same class before performing casting and comparing attributes, otherwise you can end up with a ClassCastException. And getClass() can be used for that purpose, if objects do not belong to the same class they are clearly not equal.

Natural Id vs Surrogate Id

When you're talking about "NaturalId" like ISBN-number of a book versus "id", I guess you refer to a natural key of a persistence entity versus surrogate key which is used in a relational database.

You can encounter different opinions, the general approach is to use natural id (a set of unique properties, also called business keys) in your application and ID which entity obtains after being persisted only in the database.

For more information, have a look at these questions:

The JPA hashCode() / equals() dilemma

Should the id field of a JPA entity be considered in equals and hashCode?

You can encounter hashCode() and equals() that are implemented based on surrogate id, and making a defensive null-check to guard against the case when an entity is in transient state and its id is null. According to such implementations, a transient entity would not be equal to the entity in persistent state, having the same properties (apart from non-null id). Personally, I don't think this approach is correct.

Here's a very simple example equals/hashCode implementation, ORM-related annotations and other code is omitted (note, ISBN-number exceeds the range of long and should be represented as a String and therefore needs to be compared using equals() method).

class Book {
    private Long id;     // surrogate id
    private String isbn; // natural id
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        
        Book other = (Book) o;
        return Objects.equals(this.isbn, other.isbn);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(isbn);
    }
}
  • Related