본문 바로가기

Comparable과 Comparator

뺄셈 비교의 오버플로를 확인하고 동률 원소의 처리와 점수·제목·조회수 기준을 조합하는 정렬 규칙을 살펴봅니다.

정렬은 원소를 보기 좋게 배치하는 기능이면서 TreeSet·TreeMap에서는 동등성 판단에도 영향을 줍니다.

Comparable은 타입이 대표 자연 순서를 하나 소유하고, Comparator는 사용 지점마다 다른 순서를 제공합니다.

반환값의 정확한 크기가 아니라 음수·0·양수의 부호가 순서를 뜻합니다.

예를 들어 compare(a, b) < 0이면 a가 b보다 앞서고, 0이면 이 비교 규칙에서는 동률입니다. 내림차순도 같은 부호 규칙을 사용하되 큰 값을 앞선 것으로 판정합니다. thenComparing은 앞 기준이 동률일 때만 다음 기준을 확인하는 사전식 비교입니다.


정수 뺄셈 Comparator의 순서 역전

return this.score - other.score는 간단하지만 최댓값과 음수 최솟값의 차이가 int 범위를 넘을 수 있습니다.

다음 정렬 결과는 오름차순처럼 보이지만, 별도로 계산한 최댓값과 -1의 뺄셈 비교는 잘못된 음수 부호를 출력합니다.

lab/SubtractionComparatorOverflowBug.java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public final class SubtractionComparatorOverflowBug {
    public static void main(String[] args) {
        List<Integer> values = new ArrayList<>(List.of(Integer.MAX_VALUE, -1, 0));
        values.sort((left, right) -> left - right);
        System.out.println(values);
        System.out.println("compare=" + (Integer.MAX_VALUE - (-1)));
    }
}
정렬이 맞아 보여도 뺄셈 비교의 부호는 틀릴 수 있다

원문 정렬 결과는 -1, 0, 2147483647 순서이지만 Integer.MAX_VALUE에서 -1을 뺀 비교 결과는 -2147483648입니다. 정렬 결과 한 번만 보는 검사는 오버플로를 놓칩니다.

COMPARATOR · 오버플로

정렬이 맞아 보여도 뺄셈 비교의 부호는 틀릴 수 있다

정렬 출력 한 번으로 안전성을 판단하지 않습니다. MAX_VALUE − (−1)은 int 범위를 넘습니다.

이번 정렬 결과와 별도 뺄셈의 부호 오류 두 영역은 원 main에서 정렬한 결과와 별도 한 쌍 비교입니다. 정렬은 −1,0,MAX처럼 보이지만 MAX−(−1)의 int 결과는 −2147483648로 순서를 잘못 말합니다. 한 관측으로 비교 계약 전체를 입증하지 않습니다. 원 main의 정렬 MAX, −1, 0 입력 뺄셈 비교로 sort 이번 실행의 출력 −1, 0, MAX 겉으로는 오름차순 별도의 한 쌍 비교 MAX − (−1) 수학적 차 2147483648 int 범위를 넘음 실제 int 결과 −2147483648 MAX가 작다는 음수 부호 영역은 서로 다른 검사 실선: 입력·계산·결과 한 정렬 결과로 계약 입증 불가
이번 정렬 결과와 별도 뺄셈의 부호 오류 두 영역은 원 main에서 정렬한 결과와 별도 한 쌍 비교입니다. 정렬은 −1,0,MAX처럼 보이지만 MAX−(−1)의 int 결과는 −2147483648로 순서를 잘못 말합니다. 한 관측으로 비교 계약 전체를 입증하지 않습니다. 원 main의 정렬 MAX, −1, 0 입력 뺄셈 비교로 sort 이번 실행의 출력 −1, 0, MAX 겉으로는 오름차순 별도의 한 쌍 비교 MAX − (−1) 수학적 차 2147483648 int 범위를 넘음 실제 int 결과 −2147483648 MAX가 작다는 음수 부호 영역은 서로 다른 검사 실선: 입력·계산·결과 한 정렬 결과로 계약 입증 불가

정렬 결과

실제 출력
[-1, 0, 2147483647]
판단
이 입력에서는 오름차순처럼 보임

별도 뺄셈 비교

실제 출력
compare=-2147483648
판단
2147483647 > -1
그런데 비교 부호는 음수

이 오류의 원인은 비교 대상의 차이를 int 결과에 담으려 한 것입니다.

Integer.compare는 두 값을 직접 비교해 오버플로를 피합니다.

객체 필드는 Comparator.comparingInt·comparingLong을 사용하면 의도가 보이고 박싱도 피할 수 있습니다.


Comparable의 대표 순서

PostMetric의 자연 순서를 제목 오름차순과 id 오름차순으로 정합니다.

점수 내림차순 보고서는 Comparator로 따로 만듭니다.

자연 순서를 자주 바뀌는 화면 요구에 맞추면 타입을 사용하는 모든 TreeSet·정렬 결과가 영향을 받습니다.

src/PostMetricOrdering.java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public final class PostMetricOrdering {
    public static void main(String[] args) {
        List<PostMetric> posts =
                new ArrayList<>(
                        List.of(
                                new PostMetric(3, "thread", 70, 90),
                                new PostMetric(1, "array", 40, 90),
                                new PostMetric(2, "hash", 50, 80)));
        posts.sort(null);
        System.out.println("natural=" + posts);
        Comparator<PostMetric> byScore =
                Comparator.comparingInt(PostMetric::score)
                        .reversed()
                        .thenComparingInt(PostMetric::viewCount)
                        .reversed()
                        .thenComparingInt(PostMetric::id);
        posts.sort(byScore);
        System.out.println("report=" + posts);
    }

    private record PostMetric(int id, String title, int viewCount, int score)
            implements Comparable<PostMetric> {
        public int compareTo(PostMetric other) {
            int titleOrder = title.compareTo(other.title);
            return titleOrder != 0 ? titleOrder : Integer.compare(id, other.id);
        }
    }
}

sort(null)은 자연 순서를 사용합니다.

Comparator 체인의 reversed 위치는 주의해야 합니다.

위 표현의 두 번째 reversed는 thenComparingInt(viewCount)까지 만든 전체 비교자를 다시 뒤집습니다. 따라서 실제 순서는 점수 오름차순, 조회수 내림차순, id 오름차순입니다.

각 키 비교자를 따로 뒤집어 조합하는 편이 안전합니다.

원 첫 예제에서 hash(점수80)와 thread(점수90)를 비교하면 첫 점수 reversed는 hash를 뒤로 보내지만, 두 번째 reversed가 그 결과를 다시 뒤집어 hash를 앞으로 보냅니다. 점수90으로 동률인 thread(조회수70)와 array(조회수40)는 조회수 기본 오름차순을 뒤집어 thread가 먼저입니다. thenComparing은 앞 기준이 0일 때만 다음 기준을 사용하므로 모든 필드를 한꺼번에 더하거나 평균을 내는 규칙이 아닙니다. 둘째 예제는 입력 조회수도 다르므로 같은 출력 순서가 나오는지보다 각 키 방향을 먼저 확인하세요.

src/ExplicitComparatorChain.java
import java.util.Comparator;
import java.util.List;

public final class ExplicitComparatorChain {
    public static void main(String[] args) {
        Comparator<PostMetric> scoreDesc = Comparator.comparingInt(PostMetric::score).reversed();
        Comparator<PostMetric> viewCountDesc =
                Comparator.comparingInt(PostMetric::viewCount).reversed();
        Comparator<PostMetric> order =
                scoreDesc.thenComparing(viewCountDesc).thenComparing(PostMetric::title);
        List<PostMetric> result =
                List.of(
                                new PostMetric("hash", 50, 90),
                                new PostMetric("array", 40, 90),
                                new PostMetric("thread", 60, 80))
                        .stream()
                        .sorted(order)
                        .toList();
        System.out.println(result);
    }

    private record PostMetric(String title, int viewCount, int score) {}
}
두 번째 reversed는 점수 방향까지 다시 뒤집는다

첫 예제의 자연 순서는 제목과 id 오름차순입니다. 첫 예제의 두 번째 reversed는 점수 오름차순과 조회수 내림차순을 만듭니다. 둘째 예제는 키마다 뒤집어 점수와 조회수를 모두 내림차순으로 만듭니다. 두 예제의 입력값은 서로 다릅니다.

COMPARATOR · reversed 범위

두 번째 reversed는 점수 방향까지 다시 뒤집는다

둘째 reversed는 앞의 점수·조회수 체인 전체를 뒤집습니다. 두 예제의 입력 값은 서로 다릅니다.

전체 반전과 키별 반전의 다른 범위 첫 프로그램의 natural과 report, 별도 ExplicitComparatorChain의 결과를 영역으로 나눕니다. 첫 report는 점수↑·조회수↓·id↑이고 둘째는 점수↓·조회수↓·제목↑입니다. 두 프로그램의 서로 다른 조회수 입력을 보존합니다. 첫 예제 natural 제목 다음 id 오름 array / hash / thread 실제 결과 array90·40 hash80·50 / thread90·70 첫 예제 report 두 번째 전체 reversed 점수↑ / 조회수↓ / id↑ 실제 결과 hash80·50 thread90·70 / array90·40 둘째 예제 키별 반전 각 키만 reversed 점수↓ / 조회수↓ / 제목↑ 별도 입력의 결과 hash90·50 array90·40 / thread80·60 값 표기: 점수·조회수 영역은 각 비교 규칙과 결과 둘째 프로그램의 입력은 다름
전체 반전과 키별 반전의 다른 범위 첫 프로그램의 natural과 report, 별도 ExplicitComparatorChain의 결과를 영역으로 나눕니다. 첫 report는 점수↑·조회수↓·id↑이고 둘째는 점수↓·조회수↓·제목↑입니다. 두 프로그램의 서로 다른 조회수 입력을 보존합니다. 첫 예제 natural 제목 다음 id 오름 array / hash / thread 실제 결과 array90·40 hash80·50 / thread90·70 첫 예제 report 두 번째 전체 reversed 점수↑ / 조회수↓ / id↑ 실제 결과 hash80·50 thread90·70 / array90·40 둘째 예제 키별 반전 각 키만 reversed 점수↓ / 조회수↓ / 제목↑ 별도 입력의 결과 hash90·50 array90·40 / thread80·60 값 표기: 점수·조회수 영역은 각 비교 규칙과 결과 둘째 프로그램의 입력은 다름

첫 예제
natural

비교 방향
제목 ↑
id ↑
각 예제의 실제 결과 · 제목(점수, 조회수)
array(90, 40)
hash(80, 50)
thread(90, 70)

첫 예제
report

비교 방향
점수 ↑ · 조회수 ↓
id ↑
각 예제의 실제 결과 · 제목(점수, 조회수)
hash(80, 50)
thread(90, 70)
array(90, 40)

둘째 예제
키마다 reversed()

비교 방향
점수 ↓ · 조회수 ↓
제목 ↑
각 예제의 실제 결과 · 제목(점수, 조회수)
hash(90, 50)
array(90, 40)
thread(80, 60)

비교자 0과 원소 동등성

List.sort()는 비교 결과가 0인 두 원소를 모두 유지하며 안정 정렬이므로 기존 상대 순서를 보존합니다.

TreeSet은 0을 같은 원소로 보고 두 번째 add()를 false로 만듭니다.

점수만 비교하면 같은 점수의 서로 다른 게시글을 잃습니다.

비교 규칙은 sign(compare(a,b)) == -sign(compare(b,a)), 추이성, 일관성을 지켜야 합니다.

equals와 일관되지 않은 비교자를 TreeSet에 쓸 수는 있지만 Set의 동등성 의미가 예상과 달라질 수 있어 문서화가 필요합니다.

앞의 PostMetric도 이 조건을 실제로 가질 수 있습니다. (id=1, title="array", viewCount=40, score=90)과 (id=1, title="array", viewCount=50, score=80)은 제목과 id가 같아 compareTo가 0이지만, record의 다른 필드가 달라 equals는 false입니다. 자연 순서의 TreeSet은 두 번째 값을 추가하지 않고 첫 값을 유지합니다. 이 자연 순서가 equals와 일관되지 않음을 문서화하고, 같은 게시글의 새 측정값을 갱신하려는 요구라면 Set에 다시 add하는 대신 id별 값을 교체하는 Map 등으로 모델을 정해야 합니다. List의 정렬에서는 두 값이 모두 남습니다.

null 정렬은 Comparator.nullsFirst 또는 nullsLast로 명시할 수 있습니다.

도메인이 null을 금지한다면 생성자 검증이 더 낫고, 비교자가 조용히 null을 허용하게 만들지 않습니다.

비교자 자체도 작은 값 표로 확인한다

오름차순 비교자라면 compare(a, a)는 0이고, a가 b보다 앞설 때 compare(a, b)는 음수이며 반대 호출은 양수여야 합니다.

세 값 a < b < c를 준비해 a < c도 음수인지 확인하면 추이성 실수를 찾기 쉽습니다.

여러 thenComparing을 조합한 뒤에는 동률·역순·완전 동률 사례를 각각 실행합니다.

정렬 결과만 한 번 보면 우연히 맞아 보일 수 있습니다.

비교자를 TreeSet에 사용할 계획이라면 서로 다른 식별 원소가 0으로 비교되지 않는지도 size로 확인합니다.

binarySearch에는 List를 정렬한 바로 그 비교자를 전달해야 삽입 위치와 검색 판단이 일치합니다.


정렬 API의 원본 변경 여부

Arrays.sort()는 배열을, List.sort()와 Collections.sort()는 가변 List를 제자리에서 변경합니다.

스트림의 sorted().toList()는 원본 순서를 유지하고 수정 불가능한 새 List를 반환합니다.

호출자가 원본을 계속 사용한다면 복사본 정렬을 선택합니다.

다음 BoardRankingCli의 List.getFirst()는 Java 21 이상 API입니다. Java 17로 따라 실행한다면 이 예제처럼 입력이 비어 있지 않을 때 input.get(0)으로 첫 원소를 읽을 수 있습니다. 또한 List.of로 만든 input을 직접 sort하려 하면 비수정 목록이므로 실패합니다. 여기서는 스트림으로 새 report를 만들고 input을 그대로 유지합니다.

app/BoardRankingCli.java
import java.util.Comparator;
import java.util.List;

public final class BoardRankingCli {
    public static void main(String[] args) {
        List<Entry> input =
                List.of(
                        new Entry("hash", 50, 80),
                        new Entry("array", 40, 90),
                        new Entry("thread", 60, 90));
        Comparator<Entry> ranking =
                Comparator.comparingInt(Entry::score)
                        .reversed()
                        .thenComparing(Comparator.comparingInt(Entry::viewCount).reversed())
                        .thenComparing(Entry::title);
        List<Entry> report = input.stream().sorted(ranking).toList();
        System.out.println("input-first=" + input.getFirst().title());
        System.out.println("ranking=" + report);
    }

    private record Entry(String title, int viewCount, int score) {}
}

보고서는 thread부터이고 원본은 hash부터다

sorted가 새 report를 만들어 input 등록 순서는 유지합니다. 입력·정렬 호출·두 목록 관찰을 단계로 보여 원본 비변경을 확인할 수 있습니다.

보고서는 thread부터이고 원본은 hash부터다

BoardRankingCli의 input은 hash, array, thread 순서이고 sorted().toList()로 만든 report는 thread, array, hash 순서입니다. 실제 출력은 input의 첫 제목 hash와 report 전체를 확인합니다. input 전체 순서는 원문과 API 동작을 근거로 비교합니다.

정렬 복사본 · 원본 보존

보고서는 thread부터이고 원본은 hash부터다

입력은 hash부터 그대로이고, 보고서 복사본만 점수·조회수 내림차순으로 정렬됩니다.

원 input과 새 ranking의 별도 순서 원 input은 hash,array,thread 순서이며 input-first는 hash입니다. 보고서는 같은 원소를 점수·조회수 내림차순으로 thread,array,hash 순서에 보관합니다. 원본의 제자리 정렬이나 깊은 원소 복사가 아닙니다. input 원본 등록 순서 hash80·50 array90·40 / thread90·60 원본 첫 제목 input-first=hash report 결과 sorted로 새 결과 점수↓ / 조회수↓ / 제목↑ 실제 report 순서 thread90·60 array90·40 / hash80·50 실선: 각각의 읽기·정렬 관계 report는 새 결과 목록 input을 제자리 정렬 안 함 같은 원소 참조 / 다른 목록 순서
원 input과 새 ranking의 별도 순서 원 input은 hash,array,thread 순서이며 input-first는 hash입니다. 보고서는 같은 원소를 점수·조회수 내림차순으로 thread,array,hash 순서에 보관합니다. 원본의 제자리 정렬이나 깊은 원소 복사가 아닙니다. input 원본 등록 순서 hash80·50 array90·40 / thread90·60 원본 첫 제목 input-first=hash report 결과 sorted로 새 결과 점수↓ / 조회수↓ / 제목↑ 실제 report 순서 thread90·60 array90·40 / hash80·50 실선: 각각의 읽기·정렬 관계 report는 새 결과 목록 input을 제자리 정렬 안 함 같은 원소 참조 / 다른 목록 순서

index 0

input · 제목(점수, 조회수)
hash(80, 50)
report · 제목(점수, 조회수)
thread(90, 60)

index 1

input · 제목(점수, 조회수)
array(90, 40)
report · 제목(점수, 조회수)
array(90, 40)

index 2

input · 제목(점수, 조회수)
thread(90, 60)
report · 제목(점수, 조회수)
hash(80, 50)

이 예의 조건

  • 새 정렬 결과는 같은 원소 참조이며 input 제자리 정렬/깊은 복사가 아니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원 input은 hash,array,thread 순서이며 input-first는 hash입니다. 보고서는 같은 원소를 점수·조회 수 내림차순으로 thread,array,hash 순서에 보관합니다. 원본의 제자리 정렬이나 깊은 원소 복사가 아닙니다.
6array90·40
12점수↓
13조회 수↓
14제목↑
17array90·40
22같은 원소 참조
23다른 목록 순서
원 도표의 설명과 주석
구분내용
설명원 input은 hash,array,thread 순서이며 input-first는 hash입니다. 보고서는 같은 원소를 점수·조회 수 내림차순으로 thread,array,hash 순서에 보관합니다. 원본의 제자리 정렬이나 깊은 원소 복사가 아닙니다.
전체 읽기 정보보고서는 thread부터이고 원본은 hash부터다 보고서는 thread부터이고 원본은 hash부터다 BoardRankingCli의 input은 hash, array, thread 순서이고 sorted().toList()로 만든 report는 thread, array, hash 순서입니다. 실제 출력은 input의 첫 제목 hash와 report 전체를 확인합니다. input 전체 순서는 원문과 API 동작을 근거로 비교합니다. 정렬 복사본 · 원본 보존 보고서는 thread부터이고 원본은 hash부터다 입력은 hash부터 그대로이고, 보고서 복사본만 점수·조회 수 내림차순으로 정렬됩니다. index 0 input · 제목(점수, 조회 수) hash(80, 50) report · 제목(점수, 조회 수) thread(90, 60) index 1 input · 제목(점수, 조회 수) array(90, 40) report · 제목(점수, 조회 수) array(90, 40) index 2 input · 제목(점수, 조회 수) thread(90, 60) report · 제목(점수, 조회 수) hash(80, 50)
예의 값과 변화
관찰·상황값·보충 정보변화·이유
원본 순서

등록된 input을 읽습니다.

  • input: hash80/50,array90/40,thread90/60
  • 첫 제목: hash
  • 원본 만남 순서가 별도 계약으로 남는다.
보고서 정렬

점수↓·조회 수↓·제목↑로 새 순서를 만듭니다.

  • report: thread90/60,array90/40,hash80/50
  • 보고서 목적의 순서를 새 결과에 반영한다.
두 목록 관찰

input 첫 값과 report 순서를 각각 읽습니다.

  • input-first: hash
  • report-first: thread
  • 객체 원소: 공유
  • 목록 구조 분리가 원소 깊은 독립성을 뜻하지 않는다.

보고서는 thread부터이고 원본은 hash부터다

sorted가 새 report를 만들어 input 등록 순서는 유지합니다. 입력·정렬 호출·두 목록 관찰을 단계로 보여 원본 비변경을 확인할 수 있습니다.

보고서는 thread부터이고 원본은 hash부터다

BoardRankingCli의 input은 hash, array, thread 순서이고 sorted().toList()로 만든 report는 thread, array, hash 순서입니다. 실제 출력은 input의 첫 제목 hash와 report 전체를 확인합니다. input 전체 순서는 원문과 API 동작을 근거로 비교합니다.

정렬 복사본 · 원본 보존

보고서는 thread부터이고 원본은 hash부터다

입력은 hash부터 그대로이고, 보고서 복사본만 점수·조회수 내림차순으로 정렬됩니다.

원 input과 새 ranking의 별도 순서 원 input은 hash,array,thread 순서이며 input-first는 hash입니다. 보고서는 같은 원소를 점수·조회수 내림차순으로 thread,array,hash 순서에 보관합니다. 원본의 제자리 정렬이나 깊은 원소 복사가 아닙니다. input 원본 등록 순서 hash80·50 array90·40 / thread90·60 원본 첫 제목 input-first=hash report 결과 sorted로 새 결과 점수↓ / 조회수↓ / 제목↑ 실제 report 순서 thread90·60 array90·40 / hash80·50 실선: 각각의 읽기·정렬 관계 report는 새 결과 목록 input을 제자리 정렬 안 함 같은 원소 참조 / 다른 목록 순서
원 input과 새 ranking의 별도 순서 원 input은 hash,array,thread 순서이며 input-first는 hash입니다. 보고서는 같은 원소를 점수·조회수 내림차순으로 thread,array,hash 순서에 보관합니다. 원본의 제자리 정렬이나 깊은 원소 복사가 아닙니다. input 원본 등록 순서 hash80·50 array90·40 / thread90·60 원본 첫 제목 input-first=hash report 결과 sorted로 새 결과 점수↓ / 조회수↓ / 제목↑ 실제 report 순서 thread90·60 array90·40 / hash80·50 실선: 각각의 읽기·정렬 관계 report는 새 결과 목록 input을 제자리 정렬 안 함 같은 원소 참조 / 다른 목록 순서

index 0

input · 제목(점수, 조회수)
hash(80, 50)
report · 제목(점수, 조회수)
thread(90, 60)

index 1

input · 제목(점수, 조회수)
array(90, 40)
report · 제목(점수, 조회수)
array(90, 40)

index 2

input · 제목(점수, 조회수)
thread(90, 60)
report · 제목(점수, 조회수)
hash(80, 50)

이 예의 조건

  • 새 정렬 결과는 같은 원소 참조이며 input 제자리 정렬/깊은 복사가 아니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원 input은 hash,array,thread 순서이며 input-first는 hash입니다. 보고서는 같은 원소를 점수·조회 수 내림차순으로 thread,array,hash 순서에 보관합니다. 원본의 제자리 정렬이나 깊은 원소 복사가 아닙니다.
6array90·40
12점수↓
13조회 수↓
14제목↑
17array90·40
22같은 원소 참조
23다른 목록 순서
원 도표의 설명과 주석
구분내용
설명원 input은 hash,array,thread 순서이며 input-first는 hash입니다. 보고서는 같은 원소를 점수·조회 수 내림차순으로 thread,array,hash 순서에 보관합니다. 원본의 제자리 정렬이나 깊은 원소 복사가 아닙니다.
전체 읽기 정보보고서는 thread부터이고 원본은 hash부터다 보고서는 thread부터이고 원본은 hash부터다 BoardRankingCli의 input은 hash, array, thread 순서이고 sorted().toList()로 만든 report는 thread, array, hash 순서입니다. 실제 출력은 input의 첫 제목 hash와 report 전체를 확인합니다. input 전체 순서는 원문과 API 동작을 근거로 비교합니다. 정렬 복사본 · 원본 보존 보고서는 thread부터이고 원본은 hash부터다 입력은 hash부터 그대로이고, 보고서 복사본만 점수·조회 수 내림차순으로 정렬됩니다. index 0 input · 제목(점수, 조회 수) hash(80, 50) report · 제목(점수, 조회 수) thread(90, 60) index 1 input · 제목(점수, 조회 수) array(90, 40) report · 제목(점수, 조회 수) array(90, 40) index 2 input · 제목(점수, 조회 수) thread(90, 60) report · 제목(점수, 조회 수) hash(80, 50)
예의 값과 변화
관찰·상황값·보충 정보변화·이유
원본 순서

등록된 input을 읽습니다.

  • input: hash80/50,array90/40,thread90/60
  • 첫 제목: hash
  • 원본 만남 순서가 별도 계약으로 남는다.
보고서 정렬

점수↓·조회 수↓·제목↑로 새 순서를 만듭니다.

  • report: thread90/60,array90/40,hash80/50
  • 보고서 목적의 순서를 새 결과에 반영한다.
두 목록 관찰

input 첫 값과 report 순서를 각각 읽습니다.

  • input-first: hash
  • report-first: thread
  • 객체 원소: 공유
  • 목록 구조 분리가 원소 깊은 독립성을 뜻하지 않는다.

연습 문제

기한 오름차순, 우선순위 내림차순, id 오름차순으로 게시글 검토 작업을 정렬하세요.

TreeSet에 같은 기한·우선순위지만 다른 id를 가진 검토 작업 두 개를 넣어도 모두 남아야 합니다.

검산 입력은 (id2, 2026-07-20, 우선순위3), (id1, 같은 기한·우선순위), (id3, 2026-07-19, 우선순위1)입니다. 해설 전에 예상 id 순서와 size를 적으세요. 마지막 id 비교를 빼면 앞의 두 작업이 같은 비교값 0이 되어 두 번째 입력 id1이 거부되는지도 따로 판단합니다.

기한·우선순위만 비교하는 규칙으로 같은 세 원소를 List에 넣어 안정 정렬하면 id2와 id1이 모두 남는지, 동률인 두 원소의 순서가 어떻게 되는지도 비교하세요.

정답과 해설

이 예제처럼 id가 서로 다른 작업은 마지막 id 비교까지 포함하면 모두 유지됩니다.

exercise/ReviewTaskComparatorSolution.java
import java.time.LocalDate;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

public final class ReviewTaskComparatorSolution {
    public static void main(String[] args) {
        Comparator<ReviewTask> order =
                Comparator.comparing(ReviewTask::deadline)
                        .thenComparing(Comparator.comparingInt(ReviewTask::priority).reversed())
                        .thenComparingInt(ReviewTask::id);
        Set<ReviewTask> tasks = new TreeSet<>(order);
        tasks.add(new ReviewTask(2, LocalDate.of(2026, 7, 20), 3));
        tasks.add(new ReviewTask(1, LocalDate.of(2026, 7, 20), 3));
        tasks.add(new ReviewTask(3, LocalDate.of(2026, 7, 19), 1));
        System.out.println("size=" + tasks.size() + ", order=" + tasks);
    }

    private record ReviewTask(int id, LocalDate deadline, int priority) {}
}
기한·우선순위가 같아도 id 1과 2는 모두 남는다

원문 TreeSet은 검토 작업 세 개를 모두 유지합니다. 7월 19일 작업이 먼저이고, 기한과 우선순위가 같은 7월 20일 작업은 id 1, id 2 순서로 남습니다.

TREESET · 동점 해소

기한·우선순위가 같아도 id 1과 2는 모두 남는다

기한이 먼저, 우선순위가 다음, id가 마지막 기준입니다. 마지막 기준 덕분에 원소 셋이 모두 남습니다.

기한·우선순위 동률에서 id가 구별하는 원소원 세 작업을 기한→우선순위→id 규칙으로 판단하면 id3,id1,id2이며 size3입니다. 실선은 판단 규칙과 결과 관계이지 TreeSet 내부 비교 호출 순서를 재현한 선이 아닙니다.
세 작업 입력id2:7/20·우선3id1:7/20·우선3id3:7/19·우선1
연결 관계세 작업 입력 · id2:7/20·우선3 · id1:7/20·우선3 · id3:7/19·우선1 → 기한을 먼저 비교 · 7/19의 id3가 먼저 · 나머지는 기한 동률
기한을 먼저 비교7/19의 id3가 먼저나머지는 기한 동률
연결 관계기한을 먼저 비교 · 7/19의 id3가 먼저 · 나머지는 기한 동률 → 동률에서 우선·id 비교 · 우선3으로도 동률 · id1 < id2가 구별
동률에서 우선·id 비교우선3으로도 동률id1 < id2가 구별
연결 관계동률에서 우선·id 비교 · 우선3으로도 동률 · id1 < id2가 구별 → TreeSet 실제 결과 · id3 → id1 → id2 · size=3 / 둘 다 유지
TreeSet 실제 결과id3 → id1 → id2size=3 / 둘 다 유지

실선: 비교 규칙→순서 결정실제 내부 비교 호출 순서 아님

첫 번째

id
3
기한
2026-07-19
우선순위
1

두 번째

id
1
기한
2026-07-20
우선순위
3

세 번째

id
2
기한
2026-07-20
우선순위
3

size는 3이고 7월 19일 ReviewTask가 먼저입니다.

7월 20일 동률에서는 id 1, 2 순서입니다.

id 비교를 뺀 TreeSet에서는 먼저 넣은 id2가 남고 두 번째 id1의 add가 false여서 size=2, 순서는 id3 → id2입니다. 같은 비교자를 사용한 List 정렬에서는 중복을 제거하지 않고 동률의 입력 순서를 보존하므로 세 원소가 모두 남으며 id3 → id2 → id1입니다. 마지막 기준은 정렬 방향뿐 아니라 TreeSet에서 어떤 원소를 같은 것으로 볼지도 결정합니다.

정렬 설계는 자연 순서 하나와 사용 사례 순서를 분리하고, 숫자 비교에 비교 메서드를 쓰며, 비교자 0의 Set 의미를 확인하는 세 단계로 검산합니다.