"같다"에는 두 층이 있다.
| 동일성(identity) | == | 같은 객체인가 (주소 비교) |
|---|---|---|
| 동등성(equality) | equals() | 논리적으로 같은 값인가 |
String a = new String("hi"), b = new String("hi");
a == b; // false — 다른 객체
a.equals(b); // true — 같은 값
반드시 함께 재정의한다
class Point {
final int x, y;
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
// hashCode 를 재정의하지 않았다
}
Set<Point> set = new HashSet<>();
set.add(new Point(1,1));
set.contains(new Point(1,1)); // false!
HashSet 은 먼저 hashCode 로 버킷을 찾고, 그 안에서 equals 로 비교한다
- hashCode 가 다르면 애초에 다른 버킷을 뒤진다
- equals 가 아무리 잘 짜여 있어도 만날 일이 없다
규약
- ① equals 가 true 면 hashCode 는 반드시 같아야 한다
- ② hashCode 가 같다고 equals 가 true 일 필요는 없다 (해시 충돌)
- ③ equals: 반사성 · 대칭성 · 추이성 · 일관성 · null 이면 false
가변 필드를 키로 쓰면 잃어버린다
class Member { String name; /* equals·hashCode 가 name 기반 */ }
Set<Member> set = new HashSet<>();
Member m = new Member("철수");
set.add(m);
m.name = "영희"; // 해시가 바뀌었다
set.contains(m); // false — 원래 버킷에 있는데 새 버킷을 찾는다
set.remove(m); // 지워지지 않는다 → 영원히 남는 누수
해시 컬렉션의 키는 불변이어야 한다.
손으로 쓰지 않는다
record Point(int x, int y) { } // equals·hashCode·toString 자동 생성 (Java 16+)
// 클래스라면
@Override public int hashCode() { return Objects.hash(x, y); }
@Override public boolean equals(Object o) {
return o instanceof Point p && x == p.x && y == p.y;
}
JPA 엔티티에서는 다르다
// getClass() 비교는 프록시에서 깨진다 → instanceof 를 쓴다
// id 가 null 인 상태(영속 전)를 고려해야 한다
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Member m)) return false;
return id != null && id.equals(m.getId());
}
@Override public int hashCode() { return getClass().hashCode(); } // 상수여도 규약 위반은 아니다
면접 함정
- ❌ "equals만 재정의하면 된다" → 해시 컬렉션에서 조용히 실패한다.
- ❌ "hashCode가 같으면 같은 객체" → 충돌이 있을 수 있다. 최종 판단은
equals다.