반응형

바쁜 분은 이것만

  1. "cannot be cast to class java.lang.Comparable"은 기준 없는 객체를 정렬해서 나는 에러 → list.sort(Comparator.comparing(Person::age))로 기준을 넘긴다
  2. 내림차순은 .reversed(), 2차 기준은 .thenComparing(Person::name)
  3. 비교를 a.age - b.age 뺄셈으로 쓰지 말 것 → Integer.compare 또는 comparingInt

숫자·문자열 리스트는 바로 정렬되지만, 직접 만든 객체는 무엇을 기준으로 정렬할지 알려 줘야 합니다. 방법은 Comparable과 Comparator 두 가지예요. 예제는 record Person(String name, int age)로 OpenJDK 27에서 실행했습니다.

1. 기준 없이 정렬하면: ClassCastException

List<Person> people = new ArrayList<>(List.of(
    new Person("철수", 30), new Person("영희", 25), new Person("민수", 35)));
people.sort(null);   // null = "자연 순서(Comparable)로 정렬"
System.out.println(people);
Exception in thread "main" java.lang.ClassCastException: class C2$Person cannot be cast to class java.lang.Comparable (C2$Person is in unnamed module of loader com.sun.tools.javac.launcher.MemoryClassLoader @258e2e41; java.lang.Comparable is in module java.base of loader 'bootstrap')
	at java.base/java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320)
	...

Comparator 없이 정렬하면 각 원소를 Comparable로 바꿔 compareTo를 부르려 하는데, Person은 Comparable이 아니라서 터집니다. (Collections.sort(people)로 쓰면 컴파일 에러 "no suitable method found for sort"가 나요.)

2. 방법 ①: Comparable 구현하기 (compareTo)

record Person(String name, int age) implements Comparable<Person> {
    @Override
    public int compareTo(Person o) {
        return Integer.compare(this.age, o.age);   // 나이 오름차순
    }
}
// main
Collections.sort(people);
System.out.println(people);
[Person[name=영희, age=25], Person[name=철수, age=30], Person[name=민수, age=35]]

compareTo는 내가 앞이면 음수, 같으면 0, 뒤면 양수를 돌려줍니다. 기준이 상황마다 바뀐다면 Comparator가 편해요.

3. 방법 ②: Comparator.comparing / reversed / thenComparing

List<Person> people = new ArrayList<>(List.of(
    new Person("철수", 30), new Person("영희", 25),
    new Person("민수", 35), new Person("가은", 30)));

people.sort(Comparator.comparing(Person::age));
System.out.println("나이↑      " + people);

people.sort(Comparator.comparing(Person::age).reversed());
System.out.println("나이↓      " + people);

people.sort(Comparator.comparing(Person::age).reversed()
                      .thenComparing(Person::name));
System.out.println("나이↓ 이름↑ " + people);
나이↑      [Person[name=영희, age=25], Person[name=철수, age=30], Person[name=가은, age=30], Person[name=민수, age=35]]
나이↓      [Person[name=민수, age=35], Person[name=철수, age=30], Person[name=가은, age=30], Person[name=영희, age=25]]
나이↓ 이름↑ [Person[name=민수, age=35], Person[name=가은, age=30], Person[name=철수, age=30], Person[name=영희, age=25]]
  • comparing(Person::age): age로 비교하는 Comparator. Person은 손대지 않아요.
  • .reversed(): 그때까지 만든 기준 전체를 뒤집습니다. "나이↓ 이름↑"은 reversed()를 먼저, thenComparing을 뒤에 붙여요.
  • .thenComparing(Person::name): 앞 기준이 같을 때(30살 두 명)의 2차 기준입니다.

람다로 쓰다가 .reversed()를 붙이면 컴파일 에러가 나기도 합니다.

people.sort(Comparator.comparing(p -> p.age()).reversed());
C6.java:9: error: cannot find symbol
        people.sort(Comparator.comparing(p -> p.age()).reversed());
                                               ^
  symbol:   method age()
  location: variable p of type Object

이어 붙이면 p의 타입을 추론 못 해 Object로 보기 때문이에요. Person::age를 쓰거나 comparing((Person p) -> p.age())로 타입을 적으면 정상 실행됩니다.

4. 한글 가나다순과 comparingInt

List<Person> people = new ArrayList<>(List.of(
    new Person("하늘", 20), new Person("가은", 31),
    new Person("나래", 27), new Person("Zoe", 22), new Person("alex", 40)));

people.sort(Comparator.comparing(Person::name));
System.out.println(people.stream().map(Person::name).toList());

people.sort(Comparator.comparingInt(Person::age));
System.out.println(people.stream().map(p -> p.name() + p.age()).toList());
[Zoe, alex, 가은, 나래, 하늘]
[하늘20, Zoe22, 나래27, 가은31, alex40]

String 기본 비교는 문자 코드 순서라 완성형 한글은 그대로 가나다순입니다. 영문은 대문자가 소문자보다 앞이라 "Zoe"가 "alex"보다 먼저 와요(무시하려면 comparing(Person::name, String.CASE_INSENSITIVE_ORDER)). comparingInt는 박싱 없이 int로 비교합니다.

5. null이 섞이면 NullPointerException → nullsFirst / nullsLast

List<Person> people = new ArrayList<>(Arrays.asList(
    new Person("철수", 30), new Person(null, 25), new Person("영희", 28)));
people.sort(Comparator.comparing(Person::name));
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because the return value of "java.util.function.Function.apply(Object)" is null
	at java.base/java.util.Comparator.lambda$comparing$77a9974f$1(Comparator.java:518)
	...

null을 어디에 둘지 정해 주면 해결돼요.

people.sort(Comparator.comparing(Person::name,
        Comparator.nullsLast(Comparator.naturalOrder())));
System.out.println("이름 null 뒤로: " + people);

people.sort(Comparator.comparing(Person::name,
        Comparator.nullsFirst(Comparator.naturalOrder())));
System.out.println("이름 null 앞으로: " + people);
이름 null 뒤로: [Person[name=영희, age=28], Person[name=철수, age=30], Person[name=null, age=25]]
이름 null 앞으로: [Person[name=null, age=25], Person[name=영희, age=28], Person[name=철수, age=30]]

원소 자체가 null이면 nullsLast(comparing(Person::age))처럼 바깥에 씌웁니다.

6. 흔한 실수: 뺄셈으로 비교하기 (정수 오버플로)

Person a = new Person("A", 2_000_000_000);
Person b = new Person("B", -2_000_000_000);
System.out.println("a.age - b.age = " + (a.age() - b.age()));

List<Person> people = new ArrayList<>(List.of(a, b, new Person("C", 0)));
people.sort((x, y) -> x.age() - y.age());
System.out.println("뺄셈:          " + people.stream().map(Person::name).toList());

people.sort((x, y) -> Integer.compare(x.age(), y.age()));
System.out.println("Integer.compare: " + people.stream().map(Person::name).toList());
a.age - b.age = -294967296
뺄셈:          [A, B, C]
Integer.compare: [B, C, A]

20억 − (−20억) = 40억은 int 범위(약 21억)를 넘어 음수로 뒤집힙니다. 가장 큰 A가 맨 앞에 왔죠. 에러 없이 순서만 틀어지니 Integer.compare나 comparingInt로 씁니다.

7. 같은 값의 순서는 유지될까? (정렬 안정성)

// 이름순으로 이미 정렬된 목록
List<Person> people = new ArrayList<>(List.of(
    new Person("가은", 30), new Person("나래", 25), new Person("다온", 30),
    new Person("라희", 25), new Person("마루", 30)));

people.sort(Comparator.comparingInt(Person::age));
people.forEach(p -> System.out.println(p.age() + " " + p.name()));
25 나래
25 라희
30 가은
30 다온
30 마루

나이가 같으면 원래 순서(이름순)가 남았습니다. Collections.sort와 객체 배열용 Arrays.sort는 문서에 stable(안정 정렬)이 보장돼 있고, List.sort 기본 구현도 같은 방식이에요.

정리

상황 쓰는 법
cannot be cast to ...Comparable Comparable 구현 또는 list.sort(Comparator...)
필드 기준 정렬 Comparator.comparing(Person::name)
내림차순 / 2차 기준 .reversed() / .thenComparing(...)
null 섞임 Comparator.nullsFirst / nullsLast
직접 비교 작성 뺄셈 금지, Integer.compare(a, b)

참고: Oracle Java SE API 문서 — java.util.Comparator(comparing, comparingInt, reversed, thenComparing, nullsFirst, nullsLast), java.lang.Comparable, java.util.List.sort, java.util.Collections.sort, java.lang.String.compareTo, java.lang.Integer.compare

반응형

+ Recent posts