Java Stream sorted()와 Comparator로 오름차순·내림차순 정렬 익히기

정렬은 단순히 “값을 순서대로 나열하는 기능”이 아니라, 비교 기준을 코드로 표현하는 연습에 가깝습니다. 그래서 이 주제를 정리할 때는 짧은 예제 하나보다, 어떤 상황에서 기본 오름차순으로 충분한지, 언제 람다식 정렬이 더 설명력이 좋은지, 어떻게 실행 결과를 검증해야 하는지를 함께 보는 편이 훨씬 도움이 됩니다. 학습 단계에서는 특히 “왜 이렇게 써야 하는가”를 이해해야 다음 단계인 객체 정렬, 다중 기준 정렬, 대소문자 무시 정렬로 자연스럽게 이어집니다.

Java Stream 정렬: 오름차순 vs 내림차순
Java Stream에서 sorted()의 기본 동작(오름차순)과 Comparator를 사용하여 내림차순으로 정렬하는 방식의 차이를 비교 설명하는 이미지입니다.

sorted()를 볼 때 먼저 구분해야 할 것: 정렬된 리스트가 아니라 정렬된 스트림

처음 많이 생기는 오해는 sorted()가 바로 List를 반환한다고 생각하는 점입니다. 하지만 Stream 파이프라인에서 sorted()는 정렬된 스트림을 반환합니다. 즉, 정렬은 되었지만 아직 최종 결과 형태로 모인 것은 아닙니다.

numbers.stream()
       .sorted()

이 상태는 리스트가 아니라 스트림이므로, 화면에 출력하거나 변수에 리스트 형태로 담고 싶다면 수집 단계가 필요합니다.

List<Integer> result = numbers.stream()
        .sorted()
        .collect(java.util.stream.Collectors.toList());

학습할 때는 다음 순서로 이해하면 덜 헷갈립니다.

1. 원본 컬렉션을 스트림으로 바꾼다 2. 중간 연산으로 정렬 규칙을 적용한다 3. 마지막에 원하는 자료형으로 수집한다

이 흐름이 익숙해지면 filter(), map(), sorted()를 섞어 써도 파이프라인 구조가 무너지지 않습니다.

오름차순 정렬은 기본 sorted()부터 확인하는 것이 좋다

숫자와 문자열처럼 자연 정렬이 가능한 타입은 sorted()만으로 오름차순 정렬이 됩니다. 자연 정렬이란 해당 타입이 기본적으로 비교 가능한 순서를 가진다는 뜻입니다. 예를 들어 Integer, String은 기본 비교 기준이 이미 정의되어 있습니다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class SortedAscendingBasic {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("Zebra", "Apple", "Mango", "Banana", "Cherry");

        List<String> sorted = words.stream()
                .sorted()
                .collect(Collectors.toList());

        System.out.println("원본: " + words);
        System.out.println("오름차순: " + sorted);
    }
}

이 예제에서 문자열은 사전순으로 정렬됩니다. 다만 실제 학습에서는 아래처럼 Comparator를 람다식으로 적어 보는 연습도 의미가 있습니다.

List<String> sorted = words.stream()
        .sorted((a, b) -> a.compareTo(b))
        .collect(Collectors.toList());

두 코드는 결과가 같습니다. 차이는 표현 방식입니다.

  • sorted() : 기본 오름차순을 간단히 사용
  • sorted((a, b) -> a.compareTo(b)) : 비교 기준을 코드에 드러냄

짧은 코드를 선호하는 상황도 많지만, 처음에는 두 번째 방식이 비교 원리를 이해하는 데 더 도움이 됩니다. 나중에 코드 리뷰나 설명이 필요할 때도 “왜 오름차순인지”를 바로 읽을 수 있기 때문입니다.

내림차순 정렬은 비교 순서를 뒤집는다고 이해하면 쉽다

내림차순 정렬은 sorted()만으로는 되지 않습니다. 비교 기준을 직접 바꿔야 합니다. 가장 많이 쓰는 방식은 비교 순서를 반대로 적는 것입니다.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class SortedDescendingExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(45, 23, 78, 12, 56, 89, 34, 67);

        List<Integer> sorted = numbers.stream()
                .sorted((a, b) -> b.compareTo(a))
                .collect(Collectors.toList());

        System.out.println("원본: " + numbers);
        System.out.println("내림차순: " + sorted);
    }
}
원본: [45, 23, 78, 12, 56, 89, 34, 67]
내림차순: [89, 78, 67, 56, 45, 34, 23, 12]

오름차순과 내림차순을 비교하면 차이가 더 분명해집니다.

  • 오름차순: a.compareTo(b)
  • 내림차순: b.compareTo(a)

수업 실습에서 자주 보이는 실수는 “내림차순으로 정렬해야 한다”는 사실은 알고 있는데, 손이 먼저 a.compareTo(b)를 써 버리는 경우입니다. 이럴 때 설명만 듣는 것보다 실제 출력을 확인하는 편이 훨씬 빠르게 익숙해집니다. 정렬은 눈으로 결과를 비교하면서 감각을 잡는 영역이기 때문입니다.

Comparator와 compareTo()는 비슷해 보여도 역할이 다르다

이 둘을 같은 개념으로 받아들이면 나중에 객체 정렬에서 막히기 쉽습니다.

  • compareTo() : 객체가 스스로 가진 비교 방식
  • Comparator : 외부에서 비교 기준을 만들어 전달하는 도구

문자열은 원래 compareTo()를 가지고 있어서 두 문자열의 순서를 비교할 수 있습니다.

"Apple".compareTo("Banana")

반면 스트림 정렬에서 아래처럼 쓰는 코드는 Comparator 형태로 비교 방식을 전달하는 것입니다.

.sorted((a, b) -> a.compareTo(b))

즉, 실제 비교 계산은 compareTo()가 하고, 정렬 기준의 틀은 Comparator가 담당한다고 이해하면 자연스럽습니다. 이 차이를 알아두면 이후 객체의 이름순, 점수순, 날짜순 정렬로 넘어갈 때 훨씬 수월합니다.

원본이 바뀌지 않는다는 점은 직접 출력해서 확인하는 습관이 좋다

Stream을 사용하는 정렬에서 매우 중요한 특징 하나는 원본 컬렉션을 직접 수정하지 않는다는 점입니다.

List<String> words = Arrays.asList("Zebra", "Apple", "Mango");

List<String> sorted = words.stream()
        .sorted()
        .collect(java.util.stream.Collectors.toList());

System.out.println(words);   // 원본
System.out.println(sorted);  // 정렬 결과

실행하면 원본 words는 그대로 유지되고, 정렬 결과만 새 리스트로 얻습니다. 이 점은 Collections.sort() 같은 방식과 감각이 다를 수 있어서 특히 주의가 필요합니다.

직접 코드 예제를 정리하고 실행해 보면, 이전에 컬렉션 정렬을 먼저 배운 학습자일수록 Stream 정렬도 원본을 바꿀 것이라고 예상하는 경우가 많았습니다. 그래서 저는 정렬 실습을 할 때 원본 출력과 결과 출력을 항상 같이 두는 편입니다. 그 습관 하나만으로도 디버깅 시간이 줄어듭니다.

개인적으로 자주 관찰한 헷갈림: 람다식 정렬은 짧아서 어려운 게 아니라 의미를 놓치기 쉬워서 어렵다

수업에서 학생들이 가장 빨리 복사하는 코드는 대개 sorted((a, b) -> b.compareTo(a)) 같은 한 줄입니다. 그런데 막상 “왜 내림차순인가요?”라고 물으면 비교 순서를 설명하지 못하는 경우가 적지 않습니다. 직접 실행해 보면서 확인했을 때도, 오름차순과 내림차순을 번갈아 바꿔 보는 연습을 한 학습자는 이후 객체 정렬로 확장할 때 덜 막혔고, 문법만 외운 학습자는 Comparator.reverseOrder()와 람다식 사이의 관계를 따로 기억해야 했습니다. 그래서 처음에는 짧게 끝내기보다, 한 번쯤은 비교 방향을 말로 설명해 보는 연습이 도움이 됩니다.

람다식 정렬과 Comparator 메서드 중 무엇을 먼저 써야 할까

내림차순은 람다식 외에도 아래처럼 더 짧게 표현할 수 있습니다.

List<Integer> sorted = numbers.stream()
        .sorted(java.util.Comparator.reverseOrder())
        .collect(java.util.stream.Collectors.toList());

실무에서는 이 방식도 자주 사용합니다. 읽기 쉬운 편이고 의도도 분명합니다. 다만 학습 초기에는 람다식이 더 설명력이 좋을 수 있습니다.

.sorted((a, b) -> b.compareTo(a))

왜냐하면 비교 방향이 눈에 직접 보이기 때문입니다. 학습 순서를 잡는다면 보통 이렇게 가는 편이 안정적입니다.

1. sorted()로 기본 오름차순 이해 2. 람다식 정렬로 오름차순/내림차순 비교 방식 익히기 3. Comparator.reverseOrder() 같은 축약형 익히기 4. 이후 Comparator.comparing()으로 객체 정렬 확장하기

코드를 짧게 쓰는 능력보다, 정렬 이유를 설명할 수 있는 능력이 먼저입니다.

자주 실수하는 지점을 정렬 방향 중심으로 점검해 보기

정렬이 안 되었다고 느낄 때는 코드 전체를 다시 보기 전에 몇 가지를 먼저 체크하면 좋습니다.

1. sorted()만 써 놓고 내림차순을 기대한 경우

sorted()는 기본적으로 오름차순입니다. 내림차순이라면 비교 기준을 반대로 적거나 Comparator.reverseOrder()를 사용해야 합니다.

2. 정렬 결과를 리스트로 받지 않은 경우

sorted() 뒤에는 스트림이 이어집니다. 최종 자료형이 필요하면 collect(Collectors.toList())로 모아야 합니다.

3. compareTo()의 방향을 반대로 이해한 경우

정확한 숫자를 외우는 것보다, “앞이 더 작으면 음수, 앞이 더 크면 양수” 정도를 기억하는 편이 실전에서 더 유용합니다. 그래야 방향을 바꾸는 코드를 스스로 작성할 수 있습니다.

4. 문자열 정렬에서 대소문자 차이를 고려하지 않은 경우

기본 문자열 정렬은 대소문자에 영향을 받을 수 있습니다. 예를 들어 "Apple"과 "apple"을 함께 넣으면 기대한 순서와 다르게 보일 수 있으니, 결과가 이상하게 느껴진다면 입력 데이터의 대소문자부터 확인해 보는 것이 좋습니다.

수업 실습 문제와 풀이 코드

문자열 오름차순과 숫자 내림차순을 한 번에 확인하는 연습입니다. 단순히 결과만 보지 말고, 원본 리스트가 유지되는지도 함께 확인해 보세요.

실습 문제

1. 문자열 리스트를 알파벳 오름차순으로 정렬하세요. 2. 정수 리스트를 내림차순으로 정렬하세요. 3. 원본 리스트와 정렬된 리스트를 모두 출력해 원본이 바뀌지 않는지 확인하세요.

풀이 코드

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamSortedPractice {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("Zebra", "Apple", "Mango", "Banana", "Cherry");
        List<Integer> numbers = Arrays.asList(45, 23, 78, 12, 56, 89, 34, 67);

        List<String> sortedWords = words.stream()
                .sorted((a, b) -> a.compareTo(b))
                .collect(Collectors.toList());

        List<Integer> sortedNumbers = numbers.stream()
                .sorted((a, b) -> b.compareTo(a))
                .collect(Collectors.toList());

        System.out.println("문자열 원본: " + words);
        System.out.println("문자열 오름차순: " + sortedWords);
        System.out.println("숫자 원본: " + numbers);
        System.out.println("숫자 내림차순: " + sortedNumbers);
    }
}

실행 결과

문자열 원본: [Zebra, Apple, Mango, Banana, Cherry]
문자열 오름차순: [Apple, Banana, Cherry, Mango, Zebra]
숫자 원본: [45, 23, 78, 12, 56, 89, 34, 67]
숫자 내림차순: [89, 78, 67, 56, 45, 34, 23, 12]

여기서 확인할 포인트는 세 가지입니다.

  • 문자열은 사전순 오름차순이 되었는가
  • 숫자는 큰 값부터 내림차순으로 배치되었는가
  • 원본 리스트는 그대로 남아 있는가

연습을 한 단계 더 하고 싶다면 다음 정도가 적당합니다.

  • 문자열을 내림차순으로 바꾸기
  • 숫자를 오름차순으로 바꾸기
  • "apple"과 "Apple"을 함께 넣고 정렬 순서 비교하기

정렬 코드를 볼 때 판단 기준을 남겨 두면 다음 단계가 쉬워진다

정렬은 외워서 쓰는 문법처럼 보이지만, 실제로는 “무엇을 기준으로, 어떤 방향으로 정렬할 것인가”를 코드에 적는 일입니다. 그래서 학습이 어느 정도 진행되면 아래 기준을 스스로 설명할 수 있는지 점검해 보면 좋습니다.

  • 기본 sorted()는 자연 정렬 가능한 값의 오름차순이다
  • 내림차순은 비교 방향을 반대로 두거나 Comparator.reverseOrder()를 사용한다
  • Comparator는 비교 규칙을 외부에서 넘기는 도구다
  • 스트림 정렬은 원본을 직접 수정하지 않는다
  • 실행 결과는 항상 원본과 결과를 함께 출력해 검증하는 편이 안전하다

직접 정리하면서 다시 느낀 점은, 정렬은 코드 길이보다 검증 습관이 더 중요하다는 것입니다. 특히 Stream에서는 원본 불변처럼 보이는 동작이 학습자에게 좋은 장점이 되기도 하지만, 반대로 “왜 값이 안 바뀌지?”라는 혼란을 만들기도 합니다. 그래서 실제 학습이나 실무 초반에는 결과를 눈으로 확인하는 작은 습관이 가장 확실한 기준이 됩니다.

참고 자료

  • Oracle Stream API

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Stream.html

  • Oracle Comparator API

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Comparator.html

  • Oracle Collectors API

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Collectors.html

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다