3주차 Unit 6.3 — Comparator<T> 의 외부 비교

Psj·2026년 5월 19일

F-lab

목록 보기
99/239

Unit 6.3 — Comparator<T> 의 외부 비교

F-LAB JAVA · 3주차 · Phase 6 · 객체 비교


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • Comparator<T>Comparable<T> 의 결정적 차이는?
  • 함수형 인터페이스로서의 Comparator 활용은?
  • comparing, thenComparing 의 사용법은?
  • reversed() 의 정확한 동작은?
  • naturalOrder, reverseOrder 의 역할은?
  • nullsFirst, nullsLast 의 null 처리는?
  • primitive 타입 전용 (comparingInt, comparingLong) 의 효과는?
  • Stream + Comparator 의 정렬 패턴은?
  • 외부 Comparator 의 PECS 활용은?

🎯 핵심 한 문장

Comparator<T> 는 "외부에서 객체를 비교하는 기준" 을 정의하는 함수형 인터페이스다.
Comparable 이 "자기 자신의 비교" 라면, Comparator 는 "다른 누군가의 비교 기준".
Java 8+ 의 풍부한 default/static 메서드 — comparing, thenComparing, reversed, naturalOrder 등 —
와 함수형 프로그래밍 결합으로 선언적이고 유연한 정렬 이 가능.
함수형 인터페이스라 람다나 메서드 참조로 간결하게 작성, Stream API 와 자연스럽게 결합.

비유 — 심판의 판정 기준

Comparable = 선수 자신의 점수
  - Player implements Comparable<Player>
  - "내 점수는 80점이야" (스스로 정의)
  - 한 가지 기준만

Comparator = 외부 심판의 평가 기준
  - Comparator<Player> byScore = ...
  - Comparator<Player> byHeight = ...
  - Comparator<Player> byAge = ...
  - 다양한 심판이 다양한 기준으로
  - 같은 선수도 다르게 평가

→ Comparator = 외부 비교 + 다양한 기준.


🧭 9개 섹션 로드맵

1. Comparator<T> 의 정의와 본질
2. Comparable vs Comparator 비교
3. comparing 정적 메서드
4. thenComparing 과 다중 정렬
5. reversed, naturalOrder, reverseOrder
6. null 처리 (nullsFirst, nullsLast)
7. primitive 전용 메서드
8. Stream + Comparator
9. 면접 + 자기 점검

1️⃣ Comparator 의 정의와 본질

1.1 인터페이스 정의

package java.util;

@FunctionalInterface
public interface Comparator<T> {
    
    int compare(T o1, T o2);
    
    // Java 8+ default 메서드들 (다음 섹션)
    // Java 8+ static 메서드들 (다음 섹션)
    
    boolean equals(Object obj);   // Object 의 equals (override 불필요)
}

핵심:

  • java.util 패키지
  • 함수형 인터페이스 (@FunctionalInterface)
  • 추상 메서드 1개compare(T, T)
  • Java 8+ 부터 default/static 메서드 풍부

1.2 기본 구현

// 익명 클래스
Comparator<Person> byAge = new Comparator<Person>() {
    @Override
    public int compare(Person p1, Person p2) {
        return Integer.compare(p1.getAge(), p2.getAge());
    }
};

// 람다 (Java 8+)
Comparator<Person> byAge = (p1, p2) -> Integer.compare(p1.getAge(), p2.getAge());

// 메서드 참조 (Comparator.comparing)
Comparator<Person> byAge = Comparator.comparing(Person::getAge);

// 사용
List<Person> people = ...;
people.sort(byAge);

Collections.sort(people, byAge);

1.3 함수형 인터페이스로서

// 단일 추상 메서드 (SAM) — 람다 가능
Comparator<Integer> c1 = (a, b) -> a - b;
Comparator<String> c2 = (s1, s2) -> s1.length() - s2.length();
Comparator<Shipment> c3 = (s1, s2) -> s1.getWeight().compareTo(s2.getWeight());

// Stream 과 자연스러운 결합
List<Person> sorted = people.stream()
    .sorted(Comparator.comparing(Person::getAge))
    .toList();

// 또는 List.sort 와
people.sort(Comparator.comparing(Person::getName));

1.4 Comparator 의 활용처

// 1. Collections.sort
Collections.sort(list, comparator);

// 2. List.sort (Java 8+)
list.sort(comparator);

// 3. Stream.sorted
stream.sorted(comparator);

// 4. TreeMap, TreeSet 생성자
TreeSet<Person> set = new TreeSet<>(byAge);
TreeMap<Person, String> map = new TreeMap<>(byAge);

// 5. PriorityQueue
PriorityQueue<Integer> queue = new PriorityQueue<>(Comparator.reverseOrder());

// 6. Collections.max/min
Collections.max(list, comparator);

// 7. Arrays.sort
Arrays.sort(array, comparator);

1.5 Comparable 없는 클래스의 정렬

// Comparable 없는 클래스
public class Color {
    private int r, g, b;
    // Comparable 구현 X (자연 순서 없음)
}

List<Color> colors = ...;

// Comparator 활용
Comparator<Color> byRed = Comparator.comparingInt(c -> c.r);
Comparator<Color> byBrightness = Comparator.comparingInt(c -> c.r + c.g + c.b);

colors.sort(byRed);
colors.sort(byBrightness);

// Comparable 없어도 정렬 가능

1.6 같은 클래스에 여러 Comparator

public class Shipment {
    // 자연 순서 (Comparable) — 한 가지만
}

// 다양한 정렬 기준
Comparator<Shipment> byWeight = Comparator.comparing(Shipment::getWeight);
Comparator<Shipment> byCreatedAt = Comparator.comparing(Shipment::getCreatedAt);
Comparator<Shipment> byBlNo = Comparator.comparing(Shipment::getBlNo);
Comparator<Shipment> byFare = Comparator.comparing(Shipment::getFare);

// 상황에 따라 다른 기준
shipments.sort(byWeight);     // 무게순
shipments.sort(byCreatedAt);  // 시간순
shipments.sort(byBlNo);        // BL 번호순

다양한 기준의 유연성.

1.7 자기 점검 답변

Comparator 의 본질과 Comparable 와의 차이는?

:

  • Comparator:

    • 외부에서 비교 기준 제공
    • java.util 패키지
    • 함수형 인터페이스 (람다 가능)
    • 한 클래스에 여러 Comparator
    • Comparable 없어도 정렬 가능
  • Comparable:

    • 자기 자신의 비교
    • java.lang 패키지
    • 함수형 인터페이스 아님 (관례적)
    • 한 클래스에 한 가지 자연 순서

→ "자기 기준 vs 외부 기준".


2️⃣ Comparable vs Comparator 비교

2.1 비교 표

항목ComparableComparator
패키지java.langjava.util
위치클래스 내부 (implements)외부
메서드compareTo(T)compare(T, T)
매개변수1개 (other)2개 (o1, o2)
함수형 인터페이스관례 X명시 (@FunctionalInterface)
람다 적합성부적합매우 적합
활용자연 순서외부 비교
정렬한 가지여러 가지
Collections.sort(list)사용 가능별도 인자 필요

2.2 같은 정렬의 두 방법

// 방법 1: Comparable
public class Person implements Comparable<Person> {
    private int age;
    
    @Override
    public int compareTo(Person other) {
        return Integer.compare(age, other.age);
    }
}

List<Person> people = ...;
Collections.sort(people);   // 자연 순서

// 방법 2: Comparator
public class Person {
    private int age;
    // Comparable 구현 X
}

Comparator<Person> byAge = Comparator.comparing(Person::getAge);
people.sort(byAge);

2.3 두 방법 결합

// Comparable 자연 순서 + Comparator 다른 기준
public class Person implements Comparable<Person> {
    private String name;
    private int age;
    
    @Override
    public int compareTo(Person other) {
        return name.compareTo(other.name);   // 자연: 이름
    }
}

List<Person> people = ...;

// 자연 순서로 정렬
Collections.sort(people);

// 다른 기준으로 정렬
people.sort(Comparator.comparing(Person::getAge));
people.sort(Comparator.comparing(Person::getEmail));

2.4 사용 시점

Comparable 선택:
  ✓ 자연 순서가 명확
  ✓ 한 가지 기준이 대부분
  ✓ 클래스의 정체성 일부
  ✓ 예: Integer, String, LocalDate

Comparator 선택:
  ✓ 자연 순서 없는 클래스
  ✓ 다양한 기준 필요
  ✓ 외부에서 정렬 기준 결정
  ✓ 예: Color (RGB), Coordinate (x,y)

둘 다:
  - Comparable 로 자연 순서
  - Comparator 로 다른 기준
  - 자바 표준 (Integer 의 Integer.reverseOrder())

2.5 ILIC 적용 예

@Entity
public class Shipment implements Comparable<Shipment> {
    
    @Id @GeneratedValue
    private Long id;
    private String blNo;
    private BigDecimal weight;
    private LocalDateTime createdAt;
    
    // 자연 순서 — id 기반
    @Override
    public int compareTo(Shipment other) {
        return Long.compare(this.id, other.id);
    }
}

// 다양한 Comparator
public class ShipmentComparators {
    public static final Comparator<Shipment> BY_WEIGHT = 
        Comparator.comparing(Shipment::getWeight);
    
    public static final Comparator<Shipment> BY_CREATED_AT = 
        Comparator.comparing(Shipment::getCreatedAt);
    
    public static final Comparator<Shipment> BY_BL_NO = 
        Comparator.comparing(Shipment::getBlNo);
    
    public static final Comparator<Shipment> URGENT_FIRST = 
        Comparator.comparing(Shipment::getPriority).reversed();
}

// 사용
List<Shipment> shipments = ...;

Collections.sort(shipments);                          // 자연 순서 (id)
shipments.sort(ShipmentComparators.BY_WEIGHT);        // 무게
shipments.sort(ShipmentComparators.URGENT_FIRST);     // 긴급 우선

2.6 자기 점검 답변

Comparable 과 Comparator 의 결정적 차이는?

:
1. 위치:

  • Comparable: 클래스 내부
  • Comparator: 외부
  1. 목적:

    • Comparable: 자연 순서 (한 가지)
    • Comparator: 외부 비교 (여러 가지)
  2. 활용:

    • Comparable: Collections.sort(list)
    • Comparator: Collections.sort(list, comparator)
  3. 유연성:

    • Comparable: 클래스 정의 시점
    • Comparator: 런타임 결정
  4. 함수형:

    • Comparable: 람다 부적합
    • Comparator: 람다 적합 + 메서드 참조

3️⃣ comparing 정적 메서드

3.1 comparing 의 정의

// Java 8+ 의 정적 팩토리 메서드
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor) {
    
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

분석:

  • T: 비교할 객체 타입
  • U: 비교 키의 타입 (Comparable 이어야)
  • keyExtractor: T 에서 U 추출
  • 반환: Comparator

3.2 기본 사용

// 1. 메서드 참조
Comparator<Person> byAge = Comparator.comparing(Person::getAge);

// 2. 람다
Comparator<Person> byName = Comparator.comparing(p -> p.getName());

// 3. 활용
List<Person> people = ...;
people.sort(Comparator.comparing(Person::getAge));

// 동작:
// (p1, p2) -> p1.getAge().compareTo(p2.getAge())
// → 추출한 키들을 자연 순서로 비교

3.3 다양한 키 추출

// 단순 필드
Comparator.comparing(Person::getName);

// 계산된 값
Comparator.comparing(p -> p.getFirstName() + " " + p.getLastName());

// 깊은 경로
Comparator.comparing(p -> p.getAddress().getCity());

// 가공된 값
Comparator.comparing(p -> p.getName().toLowerCase());

// 메서드 호출
Comparator.comparing(Shipment::getTotalFare);

3.4 두 번째 인자 — Comparator

// 추출 후 어떻게 비교할지 명시
public static <T, U> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor,
        Comparator<? super U> keyComparator) {
    
    return (c1, c2) -> 
        keyComparator.compare(keyExtractor.apply(c1), keyExtractor.apply(c2));
}

// 사용
Comparator<Person> byNameIgnoreCase = Comparator.comparing(
    Person::getName,
    String.CASE_INSENSITIVE_ORDER   // 대소문자 무시
);

Comparator<Person> byNameLength = Comparator.comparing(
    Person::getName,
    Comparator.comparingInt(String::length)   // 길이로 비교
);

3.5 ILIC 활용

// 단순
Comparator<Shipment> byWeight = Comparator.comparing(Shipment::getWeight);
Comparator<Shipment> byCreatedAt = Comparator.comparing(Shipment::getCreatedAt);

// 깊은 경로
Comparator<Shipment> bySenderCountry = 
    Comparator.comparing(s -> s.getSender().getCountry());

// 계산된 값
Comparator<Shipment> byTotal = 
    Comparator.comparing(s -> s.getWeight().multiply(s.getRate()));

// 대소문자 무시
Comparator<Shipment> byBlNoIgnoreCase = 
    Comparator.comparing(Shipment::getBlNo, String.CASE_INSENSITIVE_ORDER);

// 사용
List<Shipment> shipments = ...;
shipments.sort(byWeight);
shipments.sort(byBlNoIgnoreCase);

3.6 comparing 의 PECS

// 정밀 시그니처
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
//                ↑               ↑
//                T 또는 부모      U 또는 자식
//                받음 (CS)        반환 (PE)

// 활용 예
class Animal { ... }
class Dog extends Animal { ... }

Function<Animal, String> getName = Animal::getName;   // Animal → String

Comparator<Dog> byName = Comparator.comparing(getName);
//                                            ↑
//                                            Function<Animal, String>
//                                            ? super Dog = Animal ✓
//                                            ? extends Comparable = String ✓

3.7 자기 점검 답변

Comparator.comparing 의 동작은?

:

  • 시그니처:

    <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
  • 동작:

    • keyExtractor 로 비교 키 U 추출
    • U 의 자연 순서 (Comparable) 로 비교
    • Comparator 반환
  • 활용:

    • Comparator.comparing(Person::getAge)
    • 메서드 참조 + 람다
    • 두 번째 인자로 키의 Comparator 명시 가능

4️⃣ thenComparing 과 다중 정렬

4.1 thenComparing 의 정의

// Comparator 의 default 메서드
default Comparator<T> thenComparing(Comparator<? super T> other) {
    Objects.requireNonNull(other);
    return (Comparator<T> & Serializable) (c1, c2) -> {
        int res = compare(c1, c2);
        return (res != 0) ? res : other.compare(c1, c2);
    };
}

// 첫 Comparator 가 0 이면 두 번째 사용

4.2 기본 사용

// 다중 정렬: 나이 → 같으면 이름
Comparator<Person> byAgeAndName = 
    Comparator.comparing(Person::getAge)
        .thenComparing(Person::getName);

// 동작:
// (p1, p2) -> {
//     int cmp = Integer.compare(p1.age, p2.age);
//     if (cmp != 0) return cmp;
//     return p1.name.compareTo(p2.name);
// }

4.3 여러 단계 chaining

Comparator<Shipment> chained = 
    Comparator.comparing(Shipment::getPriority).reversed()   // 1순위: 긴급도 내림차순
        .thenComparing(Shipment::getCreatedAt)                // 2순위: 시간순
        .thenComparing(Shipment::getWeight)                   // 3순위: 무게순
        .thenComparing(Shipment::getBlNo);                    // 4순위: BL 번호

4.4 thenComparing 의 오버로드

// 3가지 형태

// 1. 다른 Comparator
Comparator<T> thenComparing(Comparator<? super T> other);

// 2. Function (Comparable 키)
default <U extends Comparable<? super U>> Comparator<T> thenComparing(
        Function<? super T, ? extends U> keyExtractor);

// 3. Function + Comparator
default <U> Comparator<T> thenComparing(
        Function<? super T, ? extends U> keyExtractor,
        Comparator<? super U> keyComparator);

// 사용
Comparator<Person> c = Comparator.comparing(Person::getAge)
    .thenComparing(byNameComparator)              // 1
    .thenComparing(Person::getEmail)              // 2
    .thenComparing(Person::getName, 
                  String.CASE_INSENSITIVE_ORDER);  // 3

4.5 primitive 전용

// thenComparingInt, thenComparingLong, thenComparingDouble
default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor);
default Comparator<T> thenComparingLong(ToLongFunction<? super T> keyExtractor);
default Comparator<T> thenComparingDouble(ToDoubleFunction<? super T> keyExtractor);

// 사용 — autoboxing 회피
Comparator<Person> c = Comparator.comparingInt(Person::getAge)
    .thenComparingInt(Person::getId);   // primitive, 빠름

4.6 ILIC 다중 정렬

public class ShipmentSorter {
    
    // 종합 정렬
    public static final Comparator<Shipment> COMPLEX_ORDER = 
        Comparator.comparing(Shipment::getPriority).reversed()   // 긴급 우선
            .thenComparing(Shipment::getStatus, 
                          Comparator.naturalOrder())              // 상태순
            .thenComparing(Shipment::getCreatedAt)                // 시간순
            .thenComparing(Shipment::getBlNo);                    // BL 번호
    
    // 단순 시간순
    public static final Comparator<Shipment> BY_TIME_AND_WEIGHT = 
        Comparator.comparing(Shipment::getCreatedAt)
            .thenComparing(Shipment::getWeight);
    
    // null 처리 포함
    public static final Comparator<Shipment> SAFE_ORDER = 
        Comparator.comparing(
            Shipment::getCreatedAt,
            Comparator.nullsLast(Comparator.naturalOrder())
        ).thenComparing(Shipment::getBlNo);
}

// 사용
List<Shipment> shipments = ...;
shipments.sort(ShipmentSorter.COMPLEX_ORDER);

4.7 thenComparing 의 효과

1. 가독성
   - "1순위 → 2순위 → 3순위" 명확
   - 코드 의도 즉시 파악

2. 재사용
   - 부분 Comparator 정의 후 결합
   - 다양한 조합

3. 확장
   - 새 기준 추가 쉬움
   - .thenComparing(...) 한 줄

4. PECS 활용
   - ? super T, ? extends U
   - 유연한 타입 매칭

4.8 자기 점검 답변

thenComparing 의 동작과 활용은?

:

  • 동작:

    • 첫 Comparator 가 0 (같음) 일 때 두 번째 사용
    • 단계별 정렬 우선순위
  • 3가지 형태:

    1. 다른 Comparator
    2. Function (키 추출)
    3. Function + Comparator
  • 활용:

    • 다중 필드 정렬
    • 가독성 ↑
    • 재사용 + 확장 용이
  • primitive 전용:

    • thenComparingInt/Long/Double
    • autoboxing 회피

5️⃣ reversed, naturalOrder, reverseOrder

5.1 reversed() default 메서드

// Comparator 의 default 메서드
default Comparator<T> reversed() {
    return Collections.reverseOrder(this);
}

// 사용
Comparator<Person> byAge = Comparator.comparing(Person::getAge);
Comparator<Person> byAgeDesc = byAge.reversed();

// 또는 chain
Comparator<Person> byAgeDesc = 
    Comparator.comparing(Person::getAge).reversed();

5.2 reversed 의 동작

// 동작
Comparator<Integer> asc = (a, b) -> Integer.compare(a, b);
Comparator<Integer> desc = asc.reversed();

asc.compare(5, 10);     // -5 (5 < 10)
desc.compare(5, 10);    // 5 (역방향)

List<Integer> nums = Arrays.asList(3, 1, 4, 1, 5, 9);

nums.sort(asc);         // [1, 1, 3, 4, 5, 9] 오름차순
nums.sort(desc);        // [9, 5, 4, 3, 1, 1] 내림차순

5.3 reversed 와 thenComparing 의 순서

// 주의: 어디에 reversed 를 붙이느냐가 중요

// 1. 첫 번째 정렬만 역순
Comparator<Person> c1 = 
    Comparator.comparing(Person::getAge).reversed()
        .thenComparing(Person::getName);
// 나이 내림차순, 같으면 이름 오름차순

// 2. 전체 역순
Comparator<Person> c2 = 
    Comparator.comparing(Person::getAge)
        .thenComparing(Person::getName)
        .reversed();
// 전체 순서 역전 (나이 내림차순, 같으면 이름 내림차순)

// 차이가 큼!
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Bob", 25);

c1.compare(p1, p2);   // age 같음 → name 비교 → Alice 먼저
c2.compare(p1, p2);   // age 같음 → name 비교 → Bob 먼저 (역순)

5.4 naturalOrder 정적 메서드

// 정적 팩토리
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
    return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}

// 사용
Comparator<Integer> natural = Comparator.naturalOrder();
Comparator<String> stringNatural = Comparator.<String>naturalOrder();

// Comparable 의 compareTo 활용
natural.compare(5, 10);   // -5

// 활용
List<Integer> nums = Arrays.asList(3, 1, 4);
nums.sort(Comparator.naturalOrder());   // [1, 3, 4]

5.5 reverseOrder 정적 메서드

// 정적 팩토리
public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
    return Collections.reverseOrder();
}

// 사용
Comparator<Integer> reverse = Comparator.reverseOrder();
reverse.compare(5, 10);   // 5 (역순)

List<Integer> nums = Arrays.asList(3, 1, 4);
nums.sort(Comparator.reverseOrder());   // [4, 3, 1]

5.6 naturalOrder vs reverseOrder vs reversed

// 세 가지 모두 비슷한 효과

// 1. 자연 순서
Comparator<Integer> c1 = Comparator.naturalOrder();
// 또는 (a, b) -> a.compareTo(b)

// 2. 역순 (정적)
Comparator<Integer> c2 = Comparator.reverseOrder();
// 또는 (a, b) -> b.compareTo(a)

// 3. naturalOrder + reversed
Comparator<Integer> c3 = Comparator.<Integer>naturalOrder().reversed();

// c2 == c3 동등

5.7 활용 패턴

// 1. TreeSet 역순
TreeSet<Integer> set = new TreeSet<>(Comparator.reverseOrder());
set.add(1);
set.add(2);
set.add(3);
// 내부: [3, 2, 1]

// 2. PriorityQueue 최대 힙
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
maxHeap.offer(5);
maxHeap.offer(2);
maxHeap.offer(8);
maxHeap.poll();   // 8 (최대값)

// 3. 정렬
List<Integer> nums = Arrays.asList(3, 1, 4);
nums.sort(Comparator.reverseOrder());

5.8 ILIC 활용

public class ShipmentSorters {
    
    // 최근 우선
    public static final Comparator<Shipment> RECENT_FIRST = 
        Comparator.comparing(Shipment::getCreatedAt).reversed();
    
    // 무거운 것 우선
    public static final Comparator<Shipment> HEAVY_FIRST = 
        Comparator.comparing(Shipment::getWeight).reversed();
    
    // 자연 순서 + 역순
    public static final Comparator<Shipment> BY_ID_DESC = 
        Comparator.<Shipment>naturalOrder().reversed();
}

// 사용
List<Shipment> shipments = ...;
shipments.sort(ShipmentSorters.RECENT_FIRST);

5.9 자기 점검 답변

reversed, naturalOrder, reverseOrder 의 차이는?

:

  • reversed():

    • default 메서드
    • 기존 Comparator 의 역순
    • chain 시 위치 주의
  • naturalOrder():

    • 정적 메서드
    • Comparable.compareTo 활용
    • 자연 순서
  • reverseOrder():

    • 정적 메서드
    • 자연 순서의 역순
    • naturalOrder().reversed() 와 동등

선택:

  • 새 Comparator: naturalOrder / reverseOrder
  • 기존 Comparator: reversed

6️⃣ null 처리 (nullsFirst, nullsLast)

6.1 null 처리의 문제

List<String> names = Arrays.asList("Charlie", null, "Alice", null, "Bob");

// 일반 sort
Collections.sort(names);   
// ❌ NullPointerException
// Comparable.compareTo 가 null 을 처리 못 함

6.2 nullsFirst 정적 메서드

// null 을 앞에 배치
public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator);

// 사용
Comparator<String> nullSafe = 
    Comparator.nullsFirst(Comparator.naturalOrder());

List<String> names = Arrays.asList("Charlie", null, "Alice", null, "Bob");
names.sort(nullSafe);
// → [null, null, Alice, Bob, Charlie]

6.3 nullsLast 정적 메서드

// null 을 뒤에 배치
public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator);

// 사용
Comparator<String> nullSafe = 
    Comparator.nullsLast(Comparator.naturalOrder());

names.sort(nullSafe);
// → [Alice, Bob, Charlie, null, null]

6.4 nullsFirst/Last 의 내부 구현

// 간략한 구현
public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
    return new Comparators.NullComparator<>(true, comparator);
}

// NullComparator
static final class NullComparator<T> implements Comparator<T> {
    private final boolean nullFirst;
    private final Comparator<T> real;
    
    @Override
    public int compare(T a, T b) {
        if (a == null) {
            return (b == null) ? 0 : (nullFirst ? -1 : 1);
        } else if (b == null) {
            return nullFirst ? 1 : -1;
        } else {
            return real.compare(a, b);
        }
    }
}

6.5 thenComparing 과 결합

// 다중 필드 + null 처리

public class Person {
    private String name;        // null 가능
    private Integer age;         // null 가능
}

Comparator<Person> c = 
    Comparator.comparing(
        Person::getAge,
        Comparator.nullsLast(Comparator.naturalOrder())   // age null 은 뒤
    ).thenComparing(
        Person::getName,
        Comparator.nullsFirst(Comparator.naturalOrder())   // name null 은 앞
    );

// 또는 더 깔끔하게
Comparator<Integer> ageComparator = 
    Comparator.nullsLast(Comparator.naturalOrder());

Comparator<String> nameComparator = 
    Comparator.nullsFirst(Comparator.naturalOrder());

Comparator<Person> c2 = 
    Comparator.comparing(Person::getAge, ageComparator)
        .thenComparing(Person::getName, nameComparator);

6.6 객체 자체가 null

// 객체 자체가 null 일 수도
List<Person> people = Arrays.asList(
    new Person("Alice"),
    null,
    new Person("Bob"),
    null
);

// 그냥 sort 하면
people.sort(Comparator.comparing(Person::getName));
// ❌ NullPointerException — null.getName() 시도

// 해결: nullsFirst/Last 로 감싸기
Comparator<Person> safe = 
    Comparator.nullsLast(
        Comparator.comparing(Person::getName)
    );

people.sort(safe);
// → [Person(Alice), Person(Bob), null, null]

6.7 ILIC 활용

public class ShipmentService {
    
    public List<Shipment> getSortedShipments() {
        List<Shipment> shipments = repository.findAll();
        
        // CompletedAt 이 null 일 수 있음 (미완료)
        shipments.sort(
            Comparator.comparing(
                Shipment::getCompletedAt,
                Comparator.nullsLast(Comparator.naturalOrder())
                // 미완료를 뒤로
            ).thenComparing(Shipment::getCreatedAt)
        );
        
        return shipments;
    }
}

6.8 Optional 과의 결합

// Optional 활용
Comparator<Person> byName = Comparator.comparing(
    p -> Optional.ofNullable(p.getName()).orElse(""),
    Comparator.naturalOrder()
);

// 또는 nullsLast
Comparator<Person> byName = Comparator.comparing(
    Person::getName,
    Comparator.nullsLast(Comparator.naturalOrder())
);

// 권장: nullsLast/First (의도 명확)

6.9 자기 점검 답변

Comparator 의 null 처리 방법은?

:
1. nullsFirst: null 을 앞에
2. nullsLast: null 을 뒤에
3. 결합: 기존 Comparator 를 감싸기

Comparator<String> safe = Comparator.nullsLast(Comparator.naturalOrder());

활용:

  • 객체 자체가 null 가능
  • 객체의 필드가 null 가능
  • thenComparing 과 결합

→ NullPointerException 회피의 핵심.


7️⃣ primitive 전용 메서드

7.1 primitive 전용 메서드들

// Comparator 의 정적 메서드들
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor);
public static <T> Comparator<T> comparingLong(ToLongFunction<? super T> keyExtractor);
public static <T> Comparator<T> comparingDouble(ToDoubleFunction<? super T> keyExtractor);

// thenComparing 의 primitive 버전
default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor);
default Comparator<T> thenComparingLong(ToLongFunction<? super T> keyExtractor);
default Comparator<T> thenComparingDouble(ToDoubleFunction<? super T> keyExtractor);

7.2 일반 comparing 과의 차이

// 일반 comparing — autoboxing
Comparator<Person> c1 = Comparator.comparing(Person::getAge);
// p.getAge() returns int → Integer (autobox)
// Integer.compareTo() 호출

// primitive 전용 — autoboxing 회피
Comparator<Person> c2 = Comparator.comparingInt(Person::getAge);
// p.getAge() returns int 그대로
// Integer.compare(int, int) 호출

// 성능 차이:
// c1: getAge() → autobox → compareTo() (3 단계)
// c2: getAge() → compare() (2 단계)
// 큰 데이터셋에서 약간 빠름

7.3 primitive 전용의 사용

// 단순
Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);

// chain
Comparator<Person> complex = 
    Comparator.comparingInt(Person::getAge)
        .thenComparingInt(Person::getYearOfBirth)
        .thenComparing(Person::getName);   // String 은 일반 thenComparing

// reverse + primitive
Comparator<Person> byAgeDesc = 
    Comparator.comparingInt(Person::getAge).reversed();

7.4 함수형 인터페이스 — ToIntFunction

// 자바 표준
@FunctionalInterface
public interface ToIntFunction<T> {
    int applyAsInt(T value);
}

// 사용
ToIntFunction<Person> getAge = Person::getAge;
int age = getAge.applyAsInt(person);

// Comparator.comparingInt 에 전달
Comparator<Person> c = Comparator.comparingInt(getAge);

7.5 BigDecimal 등 비-primitive

// BigDecimal — primitive 아님
Comparator<Shipment> byWeight = 
    Comparator.comparing(Shipment::getWeight);   // 일반 comparing

// LocalDate, LocalDateTime — primitive 아님
Comparator<Shipment> byCreatedAt = 
    Comparator.comparing(Shipment::getCreatedAt);

// String — primitive 아님
Comparator<Shipment> byBlNo = 
    Comparator.comparing(Shipment::getBlNo);

// int, long, double 만 primitive 전용
Comparator<Shipment> byPriority = 
    Comparator.comparingInt(Shipment::getPriority);

7.6 성능 비교

// 큰 데이터셋 가정

List<Person> people = ...;   // 100만 명

// 일반 comparing
long start = System.nanoTime();
people.sort(Comparator.comparing(Person::getAge));
long t1 = System.nanoTime() - start;

// primitive 전용
start = System.nanoTime();
people.sort(Comparator.comparingInt(Person::getAge));
long t2 = System.nanoTime() - start;

// t2 가 약 10-30% 빠름 (대략)

7.7 ILIC 활용

public class ShipmentComparators {
    
    // primitive 활용
    public static final Comparator<Shipment> BY_PRIORITY = 
        Comparator.comparingInt(Shipment::getPriority);
    
    public static final Comparator<Shipment> BY_ID = 
        Comparator.comparingLong(Shipment::getId);
    
    // 객체 활용
    public static final Comparator<Shipment> BY_CREATED_AT = 
        Comparator.comparing(Shipment::getCreatedAt);
    
    public static final Comparator<Shipment> BY_WEIGHT = 
        Comparator.comparing(Shipment::getWeight);
    
    // 결합
    public static final Comparator<Shipment> COMPLEX = 
        Comparator.comparingInt(Shipment::getPriority).reversed()
            .thenComparing(Shipment::getCreatedAt)
            .thenComparingLong(Shipment::getId);
}

7.8 자기 점검 답변

primitive 전용 Comparator 메서드의 장점은?

:

  • autoboxing 회피:

    • Comparator.comparing: int → Integer (box)
    • Comparator.comparingInt: int 그대로
  • 성능 ↑:

    • 큰 데이터셋에서 약간 빠름
    • GC 부담 ↓
  • 타입 안전성:

    • ToIntFunction 등 specialized 인터페이스
    • 컴파일 타임 검증
  • 활용:

    • 기본 타입 (int/long/double) 만
    • String, BigDecimal 등은 일반 comparing

8️⃣ Stream + Comparator

8.1 Stream.sorted

// 자연 순서
Stream<Integer> sorted = Stream.of(3, 1, 4, 1, 5)
    .sorted();
// → [1, 1, 3, 4, 5]

// Comparator 사용
Stream<Integer> reverse = Stream.of(3, 1, 4)
    .sorted(Comparator.reverseOrder());
// → [4, 3, 1]

// 객체 정렬
List<Shipment> sorted = shipments.stream()
    .sorted(Comparator.comparing(Shipment::getWeight))
    .toList();

8.2 Stream.max / min

// 최대값
Optional<Shipment> heaviest = shipments.stream()
    .max(Comparator.comparing(Shipment::getWeight));

// 최소값
Optional<Shipment> lightest = shipments.stream()
    .min(Comparator.comparing(Shipment::getWeight));

// 사용
heaviest.ifPresent(s -> System.out.println(s.getBlNo()));

// 없으면 기본값
Shipment max = heaviest.orElse(new Shipment());

8.3 다중 정렬

// 단계별 정렬
List<Shipment> sorted = shipments.stream()
    .sorted(
        Comparator.comparing(Shipment::getPriority).reversed()
            .thenComparing(Shipment::getCreatedAt)
            .thenComparing(Shipment::getBlNo)
    )
    .toList();

8.4 Stream + 그룹핑 + 정렬

// 그룹핑 후 각 그룹 정렬
Map<String, List<Shipment>> byRouteSorted = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getRoute,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                list.sort(Comparator.comparing(Shipment::getCreatedAt));
                return list;
            }
        )
    ));

8.5 정렬 + 페이지네이션

// 무거운 것 5개
List<Shipment> top5Heavy = shipments.stream()
    .sorted(Comparator.comparing(Shipment::getWeight).reversed())
    .limit(5)
    .toList();

// 페이지 (0-indexed)
int page = 2;
int pageSize = 10;
List<Shipment> pageContent = shipments.stream()
    .sorted(Comparator.comparing(Shipment::getCreatedAt))
    .skip(page * pageSize)
    .limit(pageSize)
    .toList();

8.6 정렬 + 중복 제거

// 같은 BL 번호는 한 번만
List<Shipment> unique = shipments.stream()
    .sorted(Comparator.comparing(Shipment::getCreatedAt))
    .filter(distinctByKey(Shipment::getBlNo))   // 사용자 함수
    .toList();

// distinctByKey 헬퍼
public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
    Set<Object> seen = ConcurrentHashMap.newKeySet();
    return t -> seen.add(keyExtractor.apply(t));
}

8.7 통계 + 정렬

// 가장 무거운 N 개의 합
BigDecimal topNWeight = shipments.stream()
    .sorted(Comparator.comparing(Shipment::getWeight).reversed())
    .limit(10)
    .map(Shipment::getWeight)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

// 정렬된 컬렉터
TreeMap<LocalDate, BigDecimal> dailyTotal = shipments.stream()
    .collect(Collectors.groupingBy(
        s -> s.getCreatedAt().toLocalDate(),
        TreeMap::new,   // 자동 정렬
        Collectors.reducing(
            BigDecimal.ZERO,
            Shipment::getWeight,
            BigDecimal::add
        )
    ));

8.8 parallelStream 과 정렬

// 병렬 정렬
List<Shipment> sorted = shipments.parallelStream()
    .sorted(Comparator.comparing(Shipment::getWeight))
    .toList();

// 주의:
// - 정렬은 본질적으로 순차적인 부분 있음
// - 병렬 효과 제한적
// - 큰 데이터셋에서만 이점
// - 작은 데이터는 일반 stream 이 빠를 수도

8.9 자기 점검 답변

Stream 과 Comparator 의 활용 패턴은?

:
1. sorted(comparator) — 정렬
2. max/min(comparator) — 최대/최소
3. 다중 정렬 — thenComparing 결합
4. 그룹핑 + 정렬 — TreeMap 또는 collectingAndThen
5. 페이지네이션 — skip + limit
6. 중복 제거 — distinctByKey 헬퍼
7. 통계 — reduce, Collectors.reducing
8. 병렬 — parallelStream (큰 데이터셋만)

→ Stream API 와 Comparator 의 강력한 결합.


9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
Comparator 정의?외부 비교 기준, 함수형 인터페이스
Comparable vs Comparator?자기 vs 외부, 한 가지 vs 여러 가지
comparing 동작?키 추출 + 자연 순서 비교
thenComparing?첫 0 이면 두 번째
reversed 동작?역순 Comparator 반환
reverseOrder vs reversed?정적 vs default
nullsFirst/Last?null 처리
comparingInt 의 효과?autoboxing 회피
Stream.sorted?Comparator 활용 정렬
Stream.max/min?Optional 반환
PriorityQueue 와 Comparator?우선순위 결정

9.2 자기 점검 체크리스트

기본 이해

  • Comparator 정의
  • 함수형 인터페이스 성격
  • Comparable 과 차이
  • 활용처 7가지

정적/default 메서드

  • comparing
  • thenComparing
  • reversed
  • naturalOrder
  • reverseOrder

null 처리

  • nullsFirst
  • nullsLast
  • 객체 자체 vs 필드 null
  • thenComparing 결합

primitive

  • comparingInt/Long/Double
  • thenComparingInt 등
  • autoboxing 효과
  • ToIntFunction 등

Stream

  • sorted
  • max/min
  • 다중 정렬
  • 페이지네이션
  • 그룹핑 + 정렬

9.3 추가 심화 질문

Q1: comparing 의 PECS 활용?

답:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor)
  • Function<? super T, ? extends U>:
    • 입력 T 의 부모도 OK (CS)
    • 출력 U 의 자식도 OK (PE)
  • U extends Comparable<? super U>:
    • U 가 자기 또는 부모와 비교 가능

→ 최대한의 유연성.

Q2: thenComparing 의 chain 끝없이 가능?

답:

  • 이론상 가능
  • 실용적으로 5-6 단계까지
  • 너무 많으면 가독성 ↓
  • 별도 Comparator 로 추출 권장

Q3: Comparator.equals 의 의미?

답:

public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);   // Object 의 equals
}
  • 두 Comparator 가 "같은 순서" 정의하는지
  • 거의 사용 X
  • Object.equals 로 충분

Q4: Comparator 의 직렬화?

답:

  • 보통 람다는 직렬화 X
  • 단, (Comparator<T> & Serializable) 캐스트로 가능
  • 자바 표준의 일부 Comparator 는 Serializable 구현

Q5: 람다 vs 익명 클래스 Comparator?

답:

// 람다 — 함수형 인터페이스
Comparator<Person> c1 = (p1, p2) -> p1.age - p2.age;

// 익명 클래스 — 인스턴스
Comparator<Person> c2 = new Comparator<>() {
    @Override
    public int compare(Person p1, Person p2) {
        return p1.age - p2.age;
    }
};

// 차이:
// - 람다: 메서드 참조와 결합 좋음
// - 익명: 상태 필드 가능 (드물게)
// - 람다 권장

🎯 핵심 요약 — 3줄 정리

1. Comparator = 외부 비교

  • 함수형 인터페이스
  • 다양한 기준 가능
  • Comparable 없어도 정렬

2. 풍부한 정적/default 메서드

  • comparing, thenComparing
  • reversed, naturalOrder, reverseOrder
  • nullsFirst, nullsLast
  • comparingInt/Long/Double (primitive)

3. Stream + 람다와 결합

  • 선언적 정렬
  • 메서드 참조와 자연스러움
  • 다중 정렬, 페이지네이션, 그룹핑

📚 다음으로...

Unit 6.4 — 비교의 종합 활용 (★ 마스터 깊이)

이번 Unit에서 Comparator 를 봤다면, 다음은 비교의 종합 + Phase 6 졸업.

  • equals + hashCode + Comparable + Comparator 의 통합
  • 정렬, 검색, 자료구조의 정밀
  • HashSet vs TreeSet 동작 분석
  • 비교 메커니즘의 마스터
  • Phase 6 졸업 시험

Phase 6 진행 상황

🚀 Phase 6 — 객체 비교
  ✅ Unit 6.1 equals 와 hashCode 의 계약
  ✅ Unit 6.2 Comparable<T> 의 자연 순서
  ✅ Unit 6.3 Comparator<T> 의 외부 비교 ← 여기
  ⏭ Unit 6.4 비교의 종합 활용 (마스터 깊이) — Phase 6 완주

3주차 누적 진행

✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주)
✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주)
✅ Phase 4 — 추상화의 두 도구 (4.1 ~ 4.4 완주)
✅ Phase 5 — 제네릭과 와일드카드 (5.1 ~ 5.5 완주)
🚀 Phase 6 — 객체 비교 (3/4 진행)

총: 25/43 Unit 작성 (약 58%)
profile
Software Developer

0개의 댓글