객체의 논리적 동등성을 정의하는 한 쌍의 메서드. 반드시 함께 재정의해야 한다.
기본 동작
// Object의 기본 구현
public boolean equals(Object o) { return this == o; } // 참조(주소) 비교
public native int hashCode(); // 객체 주소 기반
기본값은 "같은 객체인가" 를 본다. 우리가 원하는 것은 대개 "같은 값인가" 이므로 재정의가 필요하다.
계약 — 이것을 어기면 컬렉션이 오동작한다
- ① equals 가 true 인 두 객체는 hashCode 도 반드시 같아야 한다
- ② hashCode 가 같아도 equals 는 다를 수 있다 (충돌은 정상)
- ③ 같은 객체의 hashCode 는 실행 중 일관돼야 한다
①을 어기면 무슨 일이 일어나는가
class Point {
int x, y;
@Override public boolean equals(Object o) { /* x, y 비교 */ }
// hashCode 미구현!
}
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.contains(new Point(1, 2)); // false !!
왜 false인가 — 해시 컬렉션의 조회 과정
-
① hashCode() 로 버킷 번호를 구한다
-
② 그 버킷 안에서만 equals 로 비교한다
-
두 객체의 hashCode가 다르면 → 서로 다른 버킷을 본다
-
equals 를 부를 기회조차 없다 → "없음"
해시 값이 먼저 걸러내는 관문이기 때문에, 여기서 어긋나면 equals를 아무리 잘 만들어도 소용이 없다.
equals 구현 — 다섯 가지 성질
@Override
public boolean equals(Object o) {
if (this == o) return true; // 반사성·성능
if (o == null || getClass() != o.getClass()) // null·타입
return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
| 성질 | 뜻 |
|---|---|
| 반사성 | a.equals(a) 는 true |
| 대칭성 | a.equals(b) == b.equals(a) |
| 추이성 | a=b, b=c 이면 a=c |
| 일관성 | 값이 안 바뀌면 결과도 안 바뀐다 |
| null | a.equals(null) 은 false |
상속에서 대칭성이 깨지기 쉽다 — instanceof를 쓰면
부모.equals(자식)은 true인데 자식.equals(부모)는 false가 될 수 있다.
getClass() 비교를 쓰거나, 상속 대신 컴포지션을 쓴다.
hashCode 구현
@Override
public int hashCode() { return Objects.hash(x, y); } // 간단
equals에 쓴 필드를 그대로 써야 한다. 더 넣거나 빼면 계약이 깨진다.
@Override public int hashCode() { return 1; } // 계약은 지키지만 최악
계약상 문제는 없지만 모든 객체가 한 버킷에 몰려 O(1)이 O(n)이 된다.
가장 위험한 실수 — 가변 필드를 키로
Set<Person> set = new HashSet<>();
Person p = new Person("김철수");
set.add(p); // "김철수"의 해시 → 버킷 3에 저장
p.setName("이영희"); // 필드 변경 → 해시가 바뀐다
set.contains(p); // false! (이제 버킷 7을 찾는다)
set.remove(p); // 제거도 안 된다 → 영원히 못 지우는 유령 원소
해시 기반 컬렉션의 키로 쓰는 객체는 불변이어야 한다. String이 불변인 이유 중 하나가 이것이다.
자바가 도와주는 것들
// record — equals·hashCode·toString 자동 생성
public record Point(int x, int y) { }
// Lombok
@EqualsAndHashCode
JPA 엔티티에서는 주의해야 한다. 모든 필드를 쓰면 지연 로딩 프록시를 건드려 예상치 못한 쿼리가 나가고, ID만 쓰면 영속화 전(ID가 null) 객체들이 전부 같다고 판정된다. 비즈니스 키를 쓰거나 ID + null 체크로 다룬다.