Quick Reference
public final class User implements Comparable<User> {
private final long id;
private final String email;
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof User other)) return false;
return id == other.id && Objects.equals(email, other.email);
}
@Override
public int hashCode() {
return Objects.hash(id, email);
}
@Override
public int compareTo(User other) {
int byId = Long.compare(this.id, other.id);
return byId != 0 ? byId : this.email.compareTo(other.email);
}
}HashSet은 equals/hashCode로, TreeSet과 TreeMap은 정렬 비교 결과로 중복을 판단합니다. 기본 정렬이 값 동등성과 같은 대상을 가리킨다면 compareTo(...) == 0과 equals(...)도 같게 만드는 편이 안전합니다.
문법
어떤 비교 방식을 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 같은 객체 인스턴스인지 확인 | == |
| 값이 같은지 확인 | equals(...) |
HashMap/HashSet key로 사용 | equals와 hashCode를 함께 구현 |
| 클래스의 기본 정렬 순서 정의 | Comparable |
| 상황별 다른 정렬 기준 사용 | Comparator |
== vs equals: 참조 비교 vs 값 비교
==는 두 참조가 같은 객체를 가리키는지 보는 연산입니다. equals는 "이 두 객체를 같은 값으로 볼 것인가"를 정의하는 메서드입니다. 기본 Object.equals()는 ==와 동일하게 동작하므로, 값 객체에서는 반드시 재정의해야 합니다.
User u1 = new User(1L, "kim@example.com");
User u2 = new User(1L, "kim@example.com");
System.out.println(u1 == u2); // false (다른 객체)
System.out.println(u1.equals(u2)); // true (같은 값으로 정의)equals와 hashCode는 항상 함께
equals를 재정의했다면 반드시 hashCode도 함께 재정의해야 합니다. HashSet, HashMap 같은 hash 기반 컬렉션은 먼저 hashCode로 bucket을 찾고, 그 다음 equals로 동일성을 판정합니다. hashCode를 재정의하지 않으면 같은 값인데도 다른 bucket에 들어가 조회가 실패합니다.
// equals는 재정의했지만 hashCode를 재정의하지 않은 경우
Set<User> users = new HashSet<>();
users.add(new User(1L, "kim@example.com"));
// equals로는 같지만, hashCode가 다르면 조회 실패
boolean found = users.contains(new User(1L, "kim@example.com")); // false!Comparable: 기본 정렬 순서 정의
Comparable은 "자기 자신의 기본 정렬 기준"을 클래스 안에 넣는 방식입니다. TreeSet, Collections.sort() 같은 곳에서 자동으로 사용됩니다. 상황에 따라 정렬 기준이 바뀐다면 Comparator를 외부에서 주입하는 편이 더 유연합니다.
// Comparable — 클래스의 기본 순서
users.sort(null); // compareTo 기준 정렬
// Comparator — 화면/요구사항별 다른 기준
users.sort(Comparator.comparing(User::getEmail));
users.sort(Comparator.comparing(User::getCreatedAt).reversed());compareTo가 id만 보고 equals가 id와 email을 보면 다음 두 객체는 HashSet에서는 둘이지만 TreeSet에서는 하나가 됩니다. 이 불일치는 의도된 도메인 규칙일 때만 별도 Comparator로 드러내고, 기본 자연 정렬에는 섞지 않습니다.
User a = new User(1L, "a@example.com");
User b = new User(1L, "b@example.com");
// compareTo가 id만 비교했다면 TreeSet은 b를 중복으로 버린다.
// 현재 User는 id와 email 모두 비교하므로 두 값이 유지된다.
Set<User> users = new TreeSet<>();
users.add(a);
users.add(b);체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 같은 객체 참조인지 확인 | == |
| 값이 같은지 확인 | equals() 재정의 |
| hash 기반 컬렉션에서 올바르게 동작 | equals + hashCode 함께 재정의 |
| 기본 정렬 순서 정의 | Comparable<T> 구현 |
| 상황별 다른 정렬 전략 | Comparator 외부 주입 |
주의할 점
equals만 재정의하고 hashCode를 그대로 두면 HashSet·HashMap에서 조회가 실패합니다.
// ❌ equals만 재정의, hashCode 누락
public class Point {
int x, y;
@Override
public boolean equals(Object o) { ... } // hashCode 없음
}
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.contains(new Point(1, 2)); // false! — hashCode가 다르면 다른 bucket
// ✅ equals + hashCode 항상 함께
@Override
public int hashCode() {
return Objects.hash(x, y);
}정렬 기준(compareTo)과 동등성 기준(equals)이 크게 어긋나면 TreeSet 같은 정렬 컬렉션에서 예상과 다른 중복 판정이 나올 수 있습니다.
// compareTo는 id만 비교, equals는 id+email 비교
// TreeSet은 compareTo 결과 0이면 같은 원소처럼 취급참고 링크
3 sources