F-LAB JAVA · 3주차 · Phase 6 · 객체 비교
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Comparator<T> 와 Comparable<T> 의 결정적 차이는?comparing, thenComparing 의 사용법은?reversed() 의 정확한 동작은?naturalOrder, reverseOrder 의 역할은?nullsFirst, nullsLast 의 null 처리는?comparingInt, comparingLong) 의 효과는?
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 = 외부 비교 + 다양한 기준.
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. 면접 + 자기 점검
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)compare(T, T)// 익명 클래스
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);
// 단일 추상 메서드 (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. 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);
// 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 없어도 정렬 가능
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 번호순
→ 다양한 기준의 유연성.
Comparator 의 본질과 Comparable 와의 차이는?
답:
Comparator:
java.util 패키지Comparable:
java.lang 패키지→ "자기 기준 vs 외부 기준".
| 항목 | Comparable | Comparator |
|---|---|---|
| 패키지 | java.lang | java.util |
| 위치 | 클래스 내부 (implements) | 외부 |
| 메서드 | compareTo(T) | compare(T, T) |
| 매개변수 | 1개 (other) | 2개 (o1, o2) |
| 함수형 인터페이스 | 관례 X | 명시 (@FunctionalInterface) |
| 람다 적합성 | 부적합 | 매우 적합 |
| 활용 | 자연 순서 | 외부 비교 |
| 정렬 | 한 가지 | 여러 가지 |
| Collections.sort(list) | 사용 가능 | 별도 인자 필요 |
// 방법 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);
// 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));
Comparable 선택:
✓ 자연 순서가 명확
✓ 한 가지 기준이 대부분
✓ 클래스의 정체성 일부
✓ 예: Integer, String, LocalDate
Comparator 선택:
✓ 자연 순서 없는 클래스
✓ 다양한 기준 필요
✓ 외부에서 정렬 기준 결정
✓ 예: Color (RGB), Coordinate (x,y)
둘 다:
- Comparable 로 자연 순서
- Comparator 로 다른 기준
- 자바 표준 (Integer 의 Integer.reverseOrder())
@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); // 긴급 우선
Comparable 과 Comparator 의 결정적 차이는?
답:
1. 위치:
목적:
활용:
유연성:
함수형:
// 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));
}
분석:
// 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())
// → 추출한 키들을 자연 순서로 비교
// 단순 필드
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);
// 추출 후 어떻게 비교할지 명시
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) // 길이로 비교
);
// 단순
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);
// 정밀 시그니처
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 ✓
Comparator.comparing 의 동작은?
답:
시그니처:
<T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
동작:
활용:
Comparator.comparing(Person::getAge)// 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 이면 두 번째 사용
// 다중 정렬: 나이 → 같으면 이름
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);
// }
Comparator<Shipment> chained =
Comparator.comparing(Shipment::getPriority).reversed() // 1순위: 긴급도 내림차순
.thenComparing(Shipment::getCreatedAt) // 2순위: 시간순
.thenComparing(Shipment::getWeight) // 3순위: 무게순
.thenComparing(Shipment::getBlNo); // 4순위: BL 번호
// 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
// 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, 빠름
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);
1. 가독성
- "1순위 → 2순위 → 3순위" 명확
- 코드 의도 즉시 파악
2. 재사용
- 부분 Comparator 정의 후 결합
- 다양한 조합
3. 확장
- 새 기준 추가 쉬움
- .thenComparing(...) 한 줄
4. PECS 활용
- ? super T, ? extends U
- 유연한 타입 매칭
thenComparing 의 동작과 활용은?
답:
동작:
3가지 형태:
활용:
primitive 전용:
// 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();
// 동작
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] 내림차순
// 주의: 어디에 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 먼저 (역순)
// 정적 팩토리
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]
// 정적 팩토리
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]
// 세 가지 모두 비슷한 효과
// 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 동등
// 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());
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);
reversed, naturalOrder, reverseOrder 의 차이는?
답:
reversed():
naturalOrder():
reverseOrder():
naturalOrder().reversed() 와 동등선택:
List<String> names = Arrays.asList("Charlie", null, "Alice", null, "Bob");
// 일반 sort
Collections.sort(names);
// ❌ NullPointerException
// Comparable.compareTo 가 null 을 처리 못 함
// 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]
// 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]
// 간략한 구현
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);
}
}
}
// 다중 필드 + 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);
// 객체 자체가 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]
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;
}
}
// 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 (의도 명확)
Comparator 의 null 처리 방법은?
답:
1. nullsFirst: null 을 앞에
2. nullsLast: null 을 뒤에
3. 결합: 기존 Comparator 를 감싸기
Comparator<String> safe = Comparator.nullsLast(Comparator.naturalOrder());
활용:
→ NullPointerException 회피의 핵심.
// 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);
// 일반 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 단계)
// 큰 데이터셋에서 약간 빠름
// 단순
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();
// 자바 표준
@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);
// 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);
// 큰 데이터셋 가정
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% 빠름 (대략)
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);
}
primitive 전용 Comparator 메서드의 장점은?
답:
autoboxing 회피:
성능 ↑:
타입 안전성:
활용:
// 자연 순서
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();
// 최대값
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());
// 단계별 정렬
List<Shipment> sorted = shipments.stream()
.sorted(
Comparator.comparing(Shipment::getPriority).reversed()
.thenComparing(Shipment::getCreatedAt)
.thenComparing(Shipment::getBlNo)
)
.toList();
// 그룹핑 후 각 그룹 정렬
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;
}
)
));
// 무거운 것 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();
// 같은 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));
}
// 가장 무거운 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
)
));
// 병렬 정렬
List<Shipment> sorted = shipments.parallelStream()
.sorted(Comparator.comparing(Shipment::getWeight))
.toList();
// 주의:
// - 정렬은 본질적으로 순차적인 부분 있음
// - 병렬 효과 제한적
// - 큰 데이터셋에서만 이점
// - 작은 데이터는 일반 stream 이 빠를 수도
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 의 강력한 결합.
| 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? | 우선순위 결정 |
답:
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
Function<? super T, ? extends U>:U extends Comparable<? super U>:→ 최대한의 유연성.
답:
답:
public interface Comparator<T> {
int compare(T o1, T o2);
boolean equals(Object obj); // Object 의 equals
}
답:
(Comparator<T> & Serializable) 캐스트로 가능답:
// 람다 — 함수형 인터페이스
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;
}
};
// 차이:
// - 람다: 메서드 참조와 결합 좋음
// - 익명: 상태 필드 가능 (드물게)
// - 람다 권장
1. Comparator = 외부 비교
2. 풍부한 정적/default 메서드
3. Stream + 람다와 결합
이번 Unit에서 Comparator 를 봤다면, 다음은 비교의 종합 + Phase 6 졸업.
🚀 Phase 6 — 객체 비교
✅ Unit 6.1 equals 와 hashCode 의 계약
✅ Unit 6.2 Comparable<T> 의 자연 순서
✅ Unit 6.3 Comparator<T> 의 외부 비교 ← 여기
⏭ Unit 6.4 비교의 종합 활용 (마스터 깊이) — Phase 6 완주
✅ 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%)