1. 소개
현실에서는 객체를 비교하는 방법이 하나만으로 충분한 경우가 드뭅니다. 사용자 목록이 있다고 해봅시다. 어떤 때는 이름으로, 어떤 때는 나이로, 또 어떤 때는 성(성씨) 길이로 정렬하고 싶을 수 있습니다. 혹은 아예 여러분이 작성하지 않은 외부 클래스라서 그 클래스에 compareTo를 추가할 수 없을 수도 있습니다. 바로 이런 상황을 위해 Java에는 Comparator 인터페이스가 있습니다.
Comparable만으로는 부족한 경우
- 클래스를 수정할 수 없음(예: 서드파티 라이브러리의 클래스).
- 여러 정렬 기준이 필요(서로 다른 필드 기준).
- 비교 로직을 클래스와 분리하고 싶음(프로그램의 다른 부분에서 서로 다른 방식으로 정렬하려는 경우).
비유
Comparable이 객체의 내장된 “자연스러운 순서”라면, Comparator는 외부의 심판과 같습니다. 오늘은 이름, 내일은 나이, 모레는 이름 길이처럼 원하는 기준으로 여러분의 객체를 평가할 수 있습니다.
2. Comparator 인터페이스: 문법과 계약
인터페이스 선언
public interface Comparator<T> {
int compare(T o1, T o2);
}
compare 메서드는 다음을 반환해야 합니다:
- 첫 번째 객체가 두 번째보다 “작으면” 음수.
- 같으면 0.
- 첫 번째가 “크면” 양수.
계약은 Comparable과 동일하지만, 이제는 compareTo로 “현재 객체”와 “다른 객체”를 비교하는 대신 두 객체를 직접 비교합니다.
예시: 성(성씨) 기준 정렬용 Comparator
Person 클래스가 있다고 해봅시다:
public class Person {
private String firstName;
private String lastName;
private int age;
// 생성자와 getter
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
}
성(성씨)으로 정렬하는 컴퍼레이터를 만들어봅시다:
import java.util.Comparator;
public class LastNameComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return a.getLastName().compareTo(b.getLastName());
}
}
참고: 문자열(String)의 compareTo는 사전식(알파벳) 순으로 비교합니다.
3. Comparator 사용: 컬렉션 정렬
Comparator로 정렬하기
import java.util.*;
public class Main {
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Anna", "Kostetskaya", 25));
people.add(new Person("Boris", "Novak", 20));
people.add(new Person("Viktoriya", "Bell", 22));
// 성(성씨) 기준 정렬
Collections.sort(people, new LastNameComparator());
for (Person p : people) {
System.out.println(p.getLastName() + " " + p.getFirstName());
}
}
}
결과:
Novak Boris
Kostetskaya Anna
Bell Viktoriya
Comparator로 나이 기준 정렬
클래스가 이미 이름 기준으로 Comparable을 구현했더라도, 별도의 컴퍼레이터(예: 나이 기준)를 만들 수 있습니다:
public class AgeComparator implements Comparator<Person> {
@Override
public int compare(Person a, Person b) {
return Integer.compare(a.getAge(), b.getAge());
}
}
사용도 동일합니다:
Collections.sort(people, new AgeComparator());
결과:
Boris Novak (20)
Viktoriya Bell (22)
Anna Kostetskaya (25)
예시: 즉석에서 컴퍼레이터 선택
Collections.sort(people, new LastNameComparator()); // 성(성씨) 기준
Collections.sort(people, new AgeComparator()); // 나이 기준
4. 무명 클래스와 람다 식
별도 클래스를 선언하지 않고 즉석에서 컴퍼레이터를 만들 수 있습니다.
무명 클래스
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getFirstName().compareTo(b.getFirstName());
}
});
람다 식
Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));
또는 리스트의 List.sort 메서드를 쓰면 더 간결합니다:
people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
- 무명 클래스 — 예전 방식이라 장황합니다.
- 람다 — 현대적이고 간결합니다.
5. 예시: 다양한 기준으로 정렬
성(성씨) 길이 기준 정렬
Comparator<Person> byLastNameLength = (a, b) ->
Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);
나이 후 이름 기준(다단계) 정렬
Comparator<Person> byAgeThenName = (a, b) -> {
int cmp = Integer.compare(a.getAge(), b.getAge());
if (cmp != 0) return cmp;
return a.getFirstName().compareTo(b.getFirstName());
};
people.sort(byAgeThenName);
검색에도 Comparator 사용(예)
Comparator는 정렬뿐 아니라 정렬된 컬렉션에서의 검색에도 사용할 수 있습니다:
// people 리스트는 나이 기준으로 정렬되어 있어야 합니다!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
System.out.println("나이가 22인 사람을 찾았습니다: " + people.get(idx));
}
6. 모범 사례와 Comparator 사용 시 유의점
계약(컨트랙트)을 위반하지 마세요
- compare(a, b)가 0이면 compare(b, a)도 0이어야 합니다.
- compare(a, b) > 0이면 compare(b, a) < 0이어야 합니다.
- null 값 가능성을 고려하세요(아래 참조).
equals와 hashCode를 잊지 마세요
컴퍼레이터는 “각자 방식”으로 객체를 비교하지만, TreeSet 같은 구조나 TreeMap에서 키를 찾을 때는 컴퍼레이터의 비교 로직이 equals와 일관되는 것이 중요합니다. 그렇지 않으면 예기치 않은 결과가 발생할 수 있습니다. 예를 들어 컴퍼레이터로는 두 객체가 같다고 판단하지만, equals로는 같지 않다고 나올 수 있습니다.
null 안전 정렬
필드에 null이 있을 수 있다면 “준비된” 도우미를 사용하세요:
Comparator<Person> byLastNameNullSafe = Comparator.comparing(
Person::getLastName,
Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);
7. 알아두면 좋은 점
표: Comparable vs Comparator
| Comparable | Comparator | |
|---|---|---|
| 어디에 구현하나요? | 해당 클래스 내부 | 별도 클래스/람다 |
| 메서드 | |
|
| 몇 가지를 만들 수 있나요? | 오직 하나의 “자연스러운” | 필요한 만큼 얼마든지 |
| 사용 | |
|
| 타인이 만든 클래스에도 적용 가능? | 아니요 | 예 |
예시: 내림차순 정렬
정렬 순서를 수동으로 뒤집을 수 있습니다:
Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);
또는 reversed()를 사용할 수 있습니다:
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());
8. Comparator 사용 시 흔한 실수
오류 1: 비교 계약 위반. compare(a, b)와 compare(b, a)가 부호상 반대여야 한다는 점을 잊거나, 임의의 값을 반환하는 경우(예: 단순 차이 — a.getAge() - b.getAge()는 오버플로가 날 수 있음) 결과는 예측 불가능해집니다. 뺄셈 대신 Integer.compare를 사용하세요. 더 안전합니다.
오류 2: null 값 무시. 비교 기준이 되는 필드가 null일 수 있다면 반드시 이 경우를 처리하세요(예: Comparator.nullsFirst/Comparator.nullsLast). 그렇지 않으면 예상치 못한 순간에 NullPointerException이 발생하기 쉽습니다.
오류 3: 불안정한 정렬 기준. 같은 객체 쌍에 대해 서로 다른 값을 반환하는 컴퍼레이터(예: 난수나 자주 변하는 필드 사용)는 정렬 동작을 혼란스럽게 만듭니다.
오류 4: equals와의 불일치. compare(a, b) == 0인데 a.equals(b)가 false이면, TreeSet과 TreeMap 같은 컬렉션이 기대와 다르게 동작할 수 있습니다. 컴퍼레이터의 동등성과 equals의 동등성이 가능한 한 일치하도록 하는 것이 좋습니다.
오류 5: 타인 클래스에 컴퍼레이터 없이 정렬 시도. Comparable을 구현하지 않은 “타인의” 객체를 컴퍼레이터 없이 정렬하려 하면 컴파일 오류가 납니다. 명시적으로 컴퍼레이터를 전달하세요.
GO TO FULL VERSION