아이템 14. Comparable 을 구현할 지 고려하라
1. Comparable 인터페이스란?
Comparable
인터페이스는 객체의 자연순서(natural ordering) 를 정의할 때 사용된다.
compareTo(T o) 메서드를 구현하여 객체 간의 크기 비교가 가능하도록 만든다.
TreeSet, TreeMap, Arrays.sort(), Collections.sort() 등에서 정렬을 수행할 때 활용된다.
2. compareTo() 메서드 구현 방법
public int compareTo(T o);
예제: compareTo() 구현하기
class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age); // 나이를 기준으로 비교
}
}
사용 예시
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
people.add(new Person("Charlie", 35));
Collections.sort(people); // 나이 순 정렬
3. compareTo() 구현 시 주의할 점
3-2. 필드가 여러 개일 경우 비교 순서 정하기
@Override
public int compareTo(Person other) {
int result = Integer.compare(this.age, other.age); // 먼저 나이를 비교
if (result == 0) {
result = this.name.compareTo(other.name); // 나이가 같으면 이름 비교
}
return result;
}
3-3. Comparator 와 Comparable 비교
| Comparable | Comparator |
---|
구현 방식 | 클래스 자체에서 compareTo() 를 구현한다. | 외부에서 compare() 메서드를 제공한다. |
정렬 기준 | 고정(한 가지 기준) | 유연함 (여러 기준 지원 가능) |
예제 | class Person implements Comparable<Person>
| Comparator<Person> ageComparator = Comparator.comparingInt(Person::getAge);
|
3-4. Comparator 활용법
Comparator<Person> ageComparator = Comparator.comparingInt(Person::getAge);
Comparator<Person> nameComparator = Comparator.comparing(Person::getName);
4. 만약 compareTo() 와 equals() 가 일관되지 않는다면?
4-1. SortedSet 및 SortedMap 에서 동작 오류
TreeSet, TreeMap은 내부적으로 compareTo()를 기반으로 원소를 정렬하고, 중복을 판단한다.
그런데 compareTo() 와 equals()가 일관되지 않으면, 동일한 원소가 TreeSet에서 중복 삽입될 수 있다.
예시) compareTo()와 equals() 가 일관되지 않은 경우
class Person implements Comparable<Person> {
private String name;
private int id;
public Person(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name); // 이름만 비교
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Person)) return false;
Person other = (Person) obj;
return this.id == other.id; // ID만 비교
}
@Override
public int hashCode() {
return Integer.hashCode(id);
}
}
사용 예시 : 중복이 허용되는 경우
Set<Person> people = new TreeSet<>();
people.add(new Person("Alice", 1));
people.add(new Person("Alice", 2));
System.out.println(people.size()); // 2 (중복이 허용됨!)
4-2. Collections.sort() 와 contains() 가 다르게 동작하는 문제
List<Person> list = new ArrayList<>();
list.add(new Person("Alice", 1));
list.add(new Person("Bob", 2));
Collections.sort(list); // compareTo() 기준으로 정렬
Person target = new Person("Alice", 3); // ID가 다름
System.out.println(list.contains(target)); // false (비정상적인 결과)
4-3. 예외적으로 일관성이 필요하지 않은 경우
compareTo() 와 equals() 가 꼭 일관될 필요가 없는 경우도 있다.
예를 들어, 기본 정렬 방식(Comparable) 과 객체의 논리적 동일성을 다르게 정의해야 할 경우가 있다.
우선순위 큐 (PriorityQueue) 에서 정렬과 동등성을 분리해야 하는 경우
class Task implements Comparable<Task> {
String name;
int priority;
public Task(String name, int priority) {
this.name = name;
this.priority = priority;
}
@Override
public int compareTo(Task other) {
return Integer.compare(other.priority, this.priority); // 높은 우선순위가 먼저 오도록 내림차순 정렬
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Task)) return false;
Task other = (Task) obj;
return this.name.equals(other.name); // 이름이 같으면 같은 객체로 간주
}
}
// 사용 예시
Queue<Task> queue = new PriorityQueue<>();
queue.add(new Task("Task1", 1));
queue.add(new Task("Task2", 3));
System.out.println(queue.poll().name); // "Task2" (우선순위가 가장 높은 값 반환)
여기서 compareTo()는 우선순위(priority) 를 기준으로 정렬 하지만,
equals()는 **이름(name)**을 기준으로 동등성을 판단한다.
우선순위 큐의 정렬 기준과 동등성 비교 기준이 다를 수 있기 때문에,
이런 경우에는 compareTo()와 equals()가 일관되지 않아도 된다.
5. 결론
객체의 자연 순서(natural ordering) 가 의미 있다면 Comparable 을 구현해야 한다.
compareTo()는 equals()와 일반적으로 일관되게 구현하는 것이 바람직하다.
여러 필드를 비교할 경우 우선순위에 따라 차례로 비교해야 한다.
정렬 기준이 여러개 필요하다면 Comparator를 활용하는 것이 유용하다.
Last modified: 18 March 2025