Comparator

두 객체를 비교해서 어느 게 먼저 오는지 결정하는 함수형 인터페이스입니다 (java.util.Comparator). 정렬, TreeSet, TreeMap, PriorityQueue, Stream.sorted 등에서 순서를 정의하는 데 사용됩니다.

시그니처

@FunctionalInterface
public interface Comparator<T> {
    int compare(T a, T b);
}
반환값의미
음수a가 b보다 앞
0순서 동일
양수a가 b보다 뒤
[ 직관 ]
return a - b;   // 오름차순 (a가 작으면 음수 → a가 앞)
return b - a;   // 내림차순 (b가 작으면 음수 → b가 앞)

Comparable vs Comparator

항목Comparable<T>Comparator<T>
위치클래스 자체에 구현외부에서 정의
메서드compareTo(T other)compare(T a, T b)
의미"자연 순서""외부에서 부여하는 순서"
예Integer, String (이미 구현됨)커스텀 정렬 기준
// Comparable — 클래스에 자연 순서가 박혀있음
class Student implements Comparable<Student> {
    int score;
    public int compareTo(Student other) {
        return this.score - other.score;
    }
}

// Comparator — 외부에서 다른 기준 부여
Comparator<Student> byName = (a, b) -> a.name.compareTo(b.name);

4가지 작성 방법

1. 람다 (Java 8+)

Comparator<Integer> asc = (a, b) -> a - b;
Comparator<Integer> desc = (a, b) -> b - a;

list.sort((a, b) -> a - b);   // 가장 흔한 형태

2. 메서드 레퍼런스

Comparator<Student> byScore = Comparator.comparingInt(Student::getScore);
Comparator<Student> byName  = Comparator.comparing(Student::getName);

list.sort(Comparator.comparingInt(Student::getScore));

3. 정적 팩토리 메서드

Comparator<Integer> asc  = Comparator.naturalOrder();
Comparator<Integer> desc = Comparator.reverseOrder();

list.sort(Comparator.naturalOrder());

4. 익명 클래스 (Java 8 이전 방식)

list.sort(new Comparator<Integer>() {
    @Override
    public int compare(Integer a, Integer b) {
        return a - b;
    }
});

// → 람다로 동일한 의미 한 줄
list.sort((a, b) -> a - b);

다중 기준 정렬 — thenComparing

여러 기준으로 정렬할 때, 첫 번째 기준이 같으면 두 번째 기준을 사용.

람다 직접 작성

class Student { String name; int score; }
List<Student> students = ...;

// 점수 내림차순, 같으면 이름 오름차순
students.sort((a, b) -> {
    if (a.score != b.score) {
        return Integer.compare(b.score, a.score);   // 점수 내림차순
    }
    return a.name.compareTo(b.name);                // 이름 오름차순
});

thenComparing 체이닝 — 권장

students.sort(
    Comparator.comparingInt(Student::getScore).reversed()
        .thenComparing(Student::getName)
);
[ 동작 ]
1. comparingInt(Student::getScore)
   → 점수 오름차순 비교 함수 생성

2. .reversed()
   → 그걸 뒤집어서 점수 내림차순으로 변환

3. .thenComparing(Student::getName)
   → 점수 같을 때 이름 오름차순으로 비교

더 복잡한 예시

// 카테고리 오름차순, 가격 내림차순, 같으면 이름 오름차순
products.sort(
    Comparator.comparing(Product::getCategory)
        .thenComparing(Comparator.comparingInt(Product::getPrice).reversed())
        .thenComparing(Product::getName)
);
[ 정렬 결과 예시 ]
입력:
  ("electronics", 100, "B")
  ("books", 50, "A")
  ("electronics", 200, "C")
  ("books", 50, "B")
  ("electronics", 100, "A")

정렬 후:
  ("books",       50,  "A")    ← 카테고리 1순위
  ("books",       50,  "B")    ← 같은 카테고리, 가격 같음, 이름 오름차순
  ("electronics", 200, "C")    ← 같은 카테고리, 가격 큰 게 먼저
  ("electronics", 100, "A")    ← 가격 100, 이름 A 먼저
  ("electronics", 100, "B")

자주 쓰는 정적 메서드

메서드용도
Comparator.naturalOrder()자연 순서 (Comparable 사용)
Comparator.reverseOrder()자연 순서 역순
Comparator.comparing(keyExtractor)키 함수로 비교 (객체)
Comparator.comparingInt(...)키 함수로 비교 (int — 박싱 안 함)
Comparator.comparingLong(...)long 버전
Comparator.comparingDouble(...)double 버전
.reversed()기존 Comparator 뒤집기
.thenComparing(...)보조 기준 추가
.nullsFirst(c) / .nullsLast(c)null 처리
// null 안전 정렬
list.sort(Comparator.nullsLast(Comparator.naturalOrder()));
// null인 원소를 뒤쪽으로 보냄

comparingInt vs comparing — 박싱 비용

// comparing: 키가 Integer로 박싱됨 (느림)
list.sort(Comparator.comparing(Student::getScore));

// comparingInt: int 그대로 비교 (빠름)
list.sort(Comparator.comparingInt(Student::getScore));

→ 키가 원시 타입이면 comparingInt/comparingLong/comparingDouble 사용

자주 하는 실수

b - a 오버플로

// X a = Integer.MIN_VALUE, b = Integer.MAX_VALUE
//    → a - b 가 음수로 오버플로 → 잘못된 결과
list.sort((a, b) -> a - b);

// O 안전
list.sort((a, b) -> Integer.compare(a, b));
list.sort(Comparator.naturalOrder());
list.sort(Integer::compare);

thenComparing 타입 추론 실패

// X 컴파일러가 첫 람다의 타입을 추론 못 해 에러
list.sort(
    Comparator.comparing(s -> s.score).reversed()
        .thenComparing(s -> s.name)
);

// O 메서드 레퍼런스 사용 — 타입 추론 잘 됨
list.sort(
    Comparator.comparingInt(Student::getScore).reversed()
        .thenComparing(Student::getName)
);

// O 명시적 타입 지정
list.sort(
    Comparator.<Student>comparingInt(s -> s.score).reversed()
        .thenComparing(s -> s.name)
);

reversed의 위치

// X 두 기준 모두 뒤집힘 (의도와 다름)
Comparator.comparing(Student::getScore)
    .thenComparing(Student::getName)
    .reversed();

// O 점수만 뒤집기
Comparator.comparingInt(Student::getScore).reversed()
    .thenComparing(Student::getName);
[ reversed의 동작 ]
.reversed()는 호출 시점까지 만들어진 Comparator 전체를 뒤집음

A.thenComparing(B).reversed()  →  (A then B) 전체를 뒤집음
A.reversed().thenComparing(B)  →  A만 뒤집고 B는 그대로

음수 반환의 함정

// X boolean을 int로 잘못 변환 — 0 또는 1만 나와서 동률 다수 발생
list.sort((a, b) -> a > b ? 1 : 0);    // -1을 안 주면 정렬 깨짐

// O -1, 0, 1 셋 다 가능해야 함
list.sort((a, b) -> a > b ? 1 : (a < b ? -1 : 0));
list.sort((a, b) -> Integer.compare(a, b));   // 권장

사용처

1. 정렬

list.sort(comparator);
Collections.sort(list, comparator);
Arrays.sort(arr, comparator);
stream.sorted(comparator).collect(...);

자세한 정렬 메서드는 정렬 허브 참고.

2. TreeSet / TreeMap

// 자연 순서
TreeSet<Integer> set = new TreeSet<>();

// 커스텀 순서 (생성자에 Comparator)
TreeSet<Student> byScore = new TreeSet<>(
    Comparator.comparingInt(Student::getScore)
);

3. PriorityQueue

// 최대 힙
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());

// 객체 우선순위 큐
PriorityQueue<Task> taskQueue = new PriorityQueue<>(
    Comparator.comparingInt(Task::getPriority)
);

4. Collections.max / min

Student topStudent = Collections.max(students,
    Comparator.comparingInt(Student::getScore)
);

자주 쓰는 패턴

정렬 우선순위 명확히

// 가독성 좋음 — 한 줄에 한 기준
products.sort(
    Comparator
        .comparing(Product::getCategory)               // 1. 카테고리 (오름차순)
        .thenComparing(Product::getPrice, Comparator.reverseOrder())  // 2. 가격 내림차순
        .thenComparing(Product::getName)               // 3. 이름 (오름차순)
);

객체 ↔ 원시 타입 정렬 차이

// int[] — Comparator 못 씀
int[] arr = {3, 1, 2};
Arrays.sort(arr);   // 오름차순만 가능

// 내림차순이 필요하면 Integer[]로 박싱
Integer[] boxed = {3, 1, 2};
Arrays.sort(boxed, Comparator.reverseOrder());

// 또는 Stream으로
Integer[] sortedDesc = Arrays.stream(arr)
    .boxed()
    .sorted(Comparator.reverseOrder())
    .toArray(Integer[]::new);

문자열 정렬 변형

// 길이 기준
list.sort(Comparator.comparingInt(String::length));

// 길이 기준, 같으면 사전식
list.sort(
    Comparator.comparingInt(String::length)
        .thenComparing(Comparator.naturalOrder())
);

// 대소문자 무시
list.sort(String.CASE_INSENSITIVE_ORDER);

문자열 연결 비교 — 가장 큰 수 패턴

숫자 배열을 이어붙였을 때 가장 큰 수를 만드는 정렬 순서를 결정할 때 사용.

// a=3, b=30 → "330" vs "303" → "330"이 크므로 3을 앞에
Arrays.sort(strNumbers, (a, b) -> (b + a).compareTo(a + b));
[ 핵심 아이디어 ]
a와 b 중 어느 것을 앞에 놓을지:
  "ab" > "ba"  →  a를 앞에  →  (b+a).compareTo(a+b) < 0  →  a가 먼저
  "ba" > "ab"  →  b를 앞에  →  (b+a).compareTo(a+b) > 0  →  b가 먼저

예: a="3", b="30"
  b+a = "303"
  a+b = "330"
  "303".compareTo("330") < 0  →  a("3")가 먼저  O
// 전체 흐름
int[] numbers = {3, 30, 34, 5, 9};
String[] strs = Arrays.stream(numbers)
    .mapToObj(String::valueOf)
    .toArray(String[]::new);

Arrays.sort(strs, (a, b) -> (b + a).compareTo(a + b));   // 내림차순 연결

// 모두 0인 엣지 케이스
if (strs[0].equals("0")) return "0";

String result = String.join("", strs);   // "9534330"

주의: a - b 같은 산술 비교 불가 → 자릿수가 다른 숫자를 이어붙인 문자열 비교가 핵심.


핵심 정리

Comparator는 두 객체 중 어느 게 먼저 오는지를 음수/0/양수로 반환하는 함수형 인터페이스로, 정렬·트리 자료구조·우선순위 큐 등 순서가 필요한 모든 곳에서 사용됩니다. 람다와 메서드 레퍼런스로 간결하게 작성하고, 다중 기준은 thenComparing으로 체이닝하는 게 권장됩니다. 자주 하는 실수는 b - a 오버플로(→ Integer.compare 사용)와 reversed()의 위치(전체를 뒤집음 vs 일부만)이며, 원시 타입 키에는 comparingInt/Long/Double를 써서 박싱 비용을 피합니다.

CSLOG에서 보기