List.contains() returns 对于违反 hashCode 约定的对象为真

List.contains() returns true for object that violated hashCode contract

拥有这些实体:

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);
    }
}

最后 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();
    }
}

输出:

Some Book
true

class List returns 的 contains() 方法怎么可能在其对象的状态(评论 r,即在用于 reading/writing 的 hashCode 计算中使用其 id 到该集合中)是否已更改?从而违反了hashCode合约?

根据 List.contains 的 Javadoc:

returns true if and only if this list contains at least one element e such that (o==null ? e==null : o.equals(e)).

所以,hashCodeequals不一致也没关系。

诚然,Set.contains 在其文档中有相同的内容,hashCode 的实施始终如一。

两者的实现方式不同:HashSet 使用 hashCode 计算元素的“查找位置”,因此可能找不到不一致的元素哈希码;像 ArrayList 这样的 List 只会线性探测所有元素来找到它,而不必先检查哈希码。