Issue
Having these entities:
Review.java:
@Entity
@Setter
@Getter
public class Review {
@Id @GeneratedValue
private Long id;
private String comment;
@ManyToOne
@JoinColumn(name = "fk_book")
private Book book;
public Review() {
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Review review = (Review) o;
return Objects.equals(id, review.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
Book.java:
@Entity
@Getter
@Setter
public class Book {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany(mappedBy = "book")
private List<Review> reviews = new ArrayList<>();
public Book() {
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(id, book.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
and finally Main.java:
public class Main {
private static EntityManagerFactory getEmf(){
return Persistence.createEntityManagerFactory("h0");
}
public static void main(String[] args) {
EntityManager em = getEmf().createEntityManager();
em.getTransaction().begin();
Book b = em.find(Book.class, 1);
Review r = new Review();
r.setComment("This is a comment");
r.setBook(b);
//Object STATE 1
//before the `r` is added, its r.getId() == `null`
b.getReviews().add(r);
//Object STATE 2
//contract violation: now its r.getId() == `1`
em.persist(r);
em.getTransaction().commit();
em.close();
em = getEmf().createEntityManager();
em.getTransaction().begin();
b = em.find(Book.class, 1);
List<Review> reviews = b.getReviews();
System.out.println(reviews.get(0).getBook().getTitle());
//Object with STATE 2
//yet the contains() method returns true even with changed id
System.out.println(reviews.contains(r));
em.getTransaction().commit();
em.close();
}
}
output:
Some Book
true
How is it possible that contains()
method of class List
returns true when the state of its object (the Review r
, which is using its id
in hashCode calculation used for reading/writing into that collection) has changed? And thus the violation of the hashCode contract has occured?
Solution
According to the Javadoc of List.contains
:
returns true if and only if this list contains at least one element e such that
(o==null ? e==null : o.equals(e))
.
So, it doesn't matter if hashCode
is inconsistent with equals
.
Granted, Set.contains
has the same in its documentation, and there it does matter that hashCode
is implemented consistently.
The difference arises in how the two are implemented: a HashSet
uses the hashCode
to work out "where to look" for the element, and thus may not find an element with an inconsistent hash code; a List
such as an ArrayList
will just linearly probe over all elements to find it, without bothering to check the hash code first.
Answered By - Andy Turner