Home > Software engineering >  List.contains() returns true for object that violated hashCode contract
List.contains() returns true for object that violated hashCode contract

Time:09-28

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?

CodePudding user response:

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.

  • Related