빌드노트
Java Comparator의 reversed()를 끝에 붙이면 그룹 순서도 뒤집힌다
Java Comparator 체인 끝의 reversed()는 그룹과 점수 기준을 함께 뒤집는다. 같은 네 항목의 정렬 결과와 compare 부호를 비교해, 그룹 순서는 유지하고 보조 점수만 내림차순으로 만드는 위치를 확인한다.
Java에서 그룹은 A→B 순서로 두고 각 그룹의 점수만 내림차순으로 정렬하려는데, comparing(...).thenComparing(...).reversed()를 쓰자 B 그룹부터 나온다. 마지막 reversed()의 대상은 그때까지 합성한 비교자 전체이기 때문이다. 점수만 뒤집으려면 점수 비교자에 reversed()를 붙인 뒤 thenComparing에 넘긴다.
Comparator.reversed()의 계약 (새 탭에서 열림)은 호출 대상 비교자의 순서를 반대로 하는 것이다. 메서드 체인에서 바로 앞에 점수 기준을 적었다고 해서 점수만 적용 대상이 되지는 않는다. thenComparing이 반환한 비교자에는 이미 그룹 우선순위가 들어 있다.
같은 네 항목에 두 비교자를 적용한다
그룹 A와 B에 점수 10·30, 20·40을 배정한 합성 데이터다. Windows의 OpenJDK 17.0.2에서 실행했다. 따라 하려면 JDK 17 이상의 jshell이 필요하다. 터미널에서 jshell --execution local을 실행한 뒤 아래 코드를 붙여 넣는다. 명령을 찾지 못하면 설치한 JDK의 실행 경로와 jshell --version부터 확인한다.
byGroup은 그룹 문자열의 자연 순서, byScore는 정수 점수의 오름차순을 만든다. 두 비교자를 모두 뒤집는 위치와 점수만 뒤집는 위치를 비교한다. 마지막 네 줄은 기본 정렬, 전체 반전, 점수만 반전, 정렬 후 원본을 차례로 조회한다.
import java.util.*;
record Item(String group, int score) {}
var input = new ArrayList<>(List.of(
new Item("B", 20), new Item("A", 10),
new Item("B", 40), new Item("A", 30)));
var byGroup = Comparator.comparing(Item::group);
var byScore = Comparator.comparingInt(Item::score);
var ascending = byGroup.thenComparing(byScore);
var wholeReversed = byGroup.thenComparing(byScore).reversed();
var scoreReversed = byGroup.thenComparing(byScore.reversed());
input.stream().sorted(ascending).map(i -> i.group() + i.score()).toList()
input.stream().sorted(wholeReversed).map(i -> i.group() + i.score()).toList()
input.stream().sorted(scoreReversed).map(i -> i.group() + i.score()).toList()
input.stream().map(i -> i.group() + i.score()).toList()
JShell의 변수 생성 메시지와 $번호 ==> 표기를 제외하면 마지막 네 조회값은 다음과 같았다. A30은 A 그룹의 30점 항목이라는 뜻이다.
[A10, A30, B20, B40]
[B40, B20, A30, A10]
[A30, A10, B40, B20]
[B20, A10, B40, A30]
둘째 줄의 wholeReversed는 그룹을 B→A로, 같은 그룹의 점수도 내림차순으로 배치했다. 셋째 줄의 scoreReversed는 A→B를 유지하면서 각 그룹 안에서만 높은 점수를 먼저 놓았다. A10이 B40보다 앞에 있다는 점도 확인한다. 점수가 그룹 우선순위를 넘어서는 정렬은 아니다.
마지막 줄의 원본은 처음 넣은 순서 그대로였다. 이 예제는 변경 가능한 ArrayList에서 stream().sorted(...).toList()로 결과를 얻었다. 별도 검사에서도 원본의 순서와 원소 참조가 유지됐다. 원래 목록을 보존해야 한다면 이렇게 얻은 정렬 결과를 사용한다.
보조 기준은 그룹 비교가 0일 때 쓰인다
thenComparing의 계약 (새 탭에서 열림)에 따르면 앞선 비교 결과가 0일 때 보조 비교자가 순서를 결정한다. 여기서는 그룹이 같아야 점수를 비교한다. 그룹 비교가 이미 A를 먼저 두기로 했다면, 안쪽 점수 비교자의 reversed()까지 갈 필요가 없다.
자기 코드에서 그룹 순서가 여전히 다르다면 최종 정렬에 넘기는 비교자로 두 항목을 직접 비교한다. 같은 JShell 세션에서 다음 네 줄은 그룹이 다른 쌍, 같은 그룹의 다른 점수, 두 키가 모두 같은 쌍을 구별한다.
wholeReversed.compare(new Item("A", 10), new Item("B", 40))
scoreReversed.compare(new Item("A", 10), new Item("B", 40))
scoreReversed.compare(new Item("A", 10), new Item("A", 30))
scoreReversed.compare(new Item("A", 30), new Item("A", 30))
실행값은 차례로 1, -1, 1, 0이었다. 비교 결과가 음수면 첫 항목이 앞, 양수면 뒤다. 전체 반전에서는 A10이 B40 뒤로 밀리지만, 점수만 반전하면 A10이 앞선다. 같은 A 그룹에서는 A10이 A30 뒤로 간다. 마지막 두 항목은 그룹과 점수가 모두 같아 비교 결과도 0이다. 실제 코드에서는 정확한 1이나 -1보다 부호와 0 여부를 확인한다.
그룹이 다른 쌍부터 잘못 나오면 thenComparing(...)을 닫은 뒤의 reversed()와 실제 연결된 그룹 비교자를 확인한다. 그룹은 맞고 내부 점수만 오름차순이면 점수 비교자의 반전 위치를 확인한다. 비교자 선언을 고친 뒤에는 정렬 호출에도 그 비교자가 전달됐는지 본다.
두 키가 모두 같은 항목의 앞뒤는 이 비교자로 구분하지 않는다. 별도 실행에서는 같은 A 그룹·30점을 가진 서로 다른 두 객체를 추가로 만들었고, 두 비교자 모두 0을 반환했다. 그 둘을 목록의 순차 스트림으로 정렬했을 때는 원래의 상대 순서가 유지됐다. Stream.sorted의 안정성 계약 (새 탭에서 열림)도 순서가 있는 스트림에서 비교상 동점인 항목의 상대 순서를 보존한다고 설명한다. 따라서 여기서의 전체 반전도 동점 객체까지 무조건 뒤집는다는 뜻은 아니다.
실행 범위는 null 없는 그룹 문자열과 정수 점수를 가진 불변 항목, 목록에서 만든 순차 스트림이다. Java SE 21 문서와 실행 버전에 해당하는 Java SE 17 Comparator 문서 (새 탭에서 열림)에서 같은 합성·역순 계약을 확인했다. JDK 21에서 별도로 실행하지 않았으며, null 처리 정책이나 순서 없는 스트림·병렬 스트림의 동작은 이번 검증에 포함하지 않았다.