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 set of classes (most likely in more than one context):
Book
BookEdition
BookItem
BookOrder
(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.
In regard to checking the class of the object passed to the equals
method - it is highly advised (yet not enforced by the compiler in any way) to verify if the object is of given type before casting it to avoid a ClassCastException
. To do that instanceof
or getClass()
should be used. If the object fulfills the requirement of being of an expected type you can cast it (e.g. Book other = (Book) object;
) and only then can you access the properties of the book (libraryId, isbn, title, author) - an object of type Object
doesn't have such fields or accessors to them.
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.
Natural IDs can vary depending on a context. In case of a BookEdition
an ISBN
is a natural ID, but in case of just a Book
it would be a pair of the title and the author (as a separate class). You can read more about the concept of natural ID in Hibernate in the docs.
It is important to understand that if you have a table in the database, it can be mapped to different types of objects in a more complex domain. ORM tools should help us with management and mapping of data, but the objects defined as data representation are (or rather: usually should be) a different layer of abstraction than the domain model.
Yet if you were forced to use, for example, the BookItem
as your data-modeling class, libraryId
could probably be an ID in the database context, but isbn
would not be a natural ID, since it does not uniquely identify the BookItem
. If BookEdition
was the data-modeling class, it could contain an ID autogenerated by the database (ID in the database context) and an ISBN, which in this case would be the natural ID as it uniquely identifies a BookEdition
in the book editions context.
To avoid such problems and make the code more flexible and descriptive, I'd suggest treating data as data and domain as domain, which is related to domain-driven design. A natural ID (as a concept) is present only on the domain level of the code as it can vary and evolve and you can still use the same database table to map the data into those various objects, depending on the business context.
Here's a code snippet with the classes described above and a class representing a table row from the database.
Data model (might be managed by an ORM like Hibernate):
// database table representation (represents data, is not a domain object)
// getters and hashCode() omitted in all classes for simplicity
class BookRow {
private long id;
private String isbn;
private String title;
// author should be a separate table joined by FK - done this way for simplification
private String authorName;
private String authorSurname;
// could have other fields as well - e.g. date of addition to the library
private Timestamp addedDate;
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
BookRow book = (BookRow) object;
// id identifies the ORM entity (a row in the database table represented as a Java object)
return id == book.id;
}
}
Domain model:
// getters and hashCode() omitted in all classes for simplicity
class Book {
private String title;
private String author;
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
Book book = (Book) object;
// title and author identify the book
return title.equals(book.title)
&& author.equals(book.author);
}
static Book fromDatabaseRow(BookRow bookRow) {
var book = new Book();
book.title = bookRow.title;
book.author = bookRow.authorName + " " + bookRow.authorSurname;
return book;
}
}
class BookEdition {
private String title;
private String author;
private String isbn;
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
BookEdition book = (BookEdition) object;
// isbn identifies the book edition
return isbn.equals(book.isbn);
}
static BookEdition fromDatabaseRow(BookRow bookRow) {
var edition = new BookEdition();
edition.title = bookRow.title;
edition.author = bookRow.authorName + " " + bookRow.authorSurname;
edition.isbn = bookRow.isbn;
return edition;
}
}
class BookItem {
private long libraryId;
private String title;
private String author;
private String isbn;
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
BookItem book = (BookItem) object;
// libraryId identifies the book item in the library system
return libraryId == book.libraryId;
}
static BookItem fromDatabaseRow(BookRow bookRow) {
var item = new BookItem();
item.libraryId = bookRow.id;
item.title = bookRow.title;
item.author = bookRow.authorName + " " + bookRow.authorSurname;
item.isbn = bookRow.isbn;
return item;
}
}