[Java] 객체 정렬의 핵심: Comparator & Comparable

bien·2025년 11월 30일

java

목록 보기
13/13

자바에서 객체를 정렬할 때 가장 중요한 것은 "누가 더 큰지 판별하는 기준"을 정하는 것이다. 정렬 알고리즘이 아무리 훌륭해도, 객체 간의 우위를 가릴 수 없다면 줄을 세울 수 없기 때문이다.

이 "판별 기준"을 제공하는 두 가지 핵심 인터페이스, Comparable과 Comparator에 대해 정리해보자.

1. 정의 및 차이점

두 인터페이스의 가장 쉬운 구별법은 누가 비교하는가? 이다.

  • Comparable:
    • 정의: 정렬 수행 시 기본적으로 적용되는 정렬 기준을 정의하는 인터페이스.
    • 구현 위치: 정렬할 객체(클래스) 내부 (implements Comparable)
    • 메서드: int compareTo(T o)
  • Comparator:
    • 정의: 기본 정렬 기준 외에 다른 기준으로 정렬하고자 할 때 사용하는 인터페이스.
    • 구현 위치: 별도의 클래스 혹은 익명 객체로 구현.
    • 메서드: int compare(T o1, T o2)


2. Comparable: "나를 기준으로 비교한다"

Comparable은 객체 스스로에게 "비교할 수 있는 능력"을 부여하는 것이다. 보통 Integer, String 같은 클래스들이 기본적으로 구현하고 있다.

인터페이스 정의

public interface Comparable<T> {
	// 나 자신(this)과 상대방(o)을 비교
    int compareTo(T o);
}

반환값(int)의 의미와 동작원리

정렬 알고리즘(시스템)은 compare 메서드가 반환하는 int값의 부호를 보고 두 객체의 위치를 바꿀지 말지 결정한다. 가장 중요한 핵심은 "양수면 자리를 바꾼다(Swap)"는 것이다.

  • compare의 반환값이
    • 양수(+): 왼쪽(또는 this)이 크다
      • 오름차순을 위해 큰 값을 뒤로 보내야 한다.
      • 자리를 바꾼다. (Swap)
    • 0: 두 값이 같다
      • 자리를 유지한다.
    • 음수(-): 오른쪽(또는 매개변수)이 크다
      • 작은 값이 이미 앞에 있다.
      • 자리를 유지한다.
  • compare()comareTo()는 두 객체의 비교결과를 반환하도록 작성한다.
    • 둘 다 반환값의 부호가 중요하다.
    • 같으면 0, 오른쪽이 크면 음수(-), 작으면 양수(+)

헷갈리지 않는 실전 구현 공식

반환값을 구할 때 누구에서 누구를 빼야 할지 헷갈린다면 아래 공식을 기억하자.

  1. 오름차순 (기본)
    • 공식: (앞의 값) - (뒤의 값)
    • 직관적으로 "내가 더 크면 양수가 되어 뒤로 밀려난다"고 이해하면 된다.
// this(앞) - o(뒤)
return this.value - o.value;
  1. 내림차순
    • 공식: (뒤의 값) - (앞의 값)
    • 오름차순의 반대이므로 순서를 바꿔서 뺄셈한다.
// o(뒤) - this(앞)
return o.value - this.value;

⚠️ 주의: 오버플로우(Overflow)

단순 뺄셈(-) 방식은 값의 차이가 int 범위를 벗어날 경우 정렬이 꼬일 수 있다. 실무에서는 안전하게 compare 메서드를 사용하는 것을 권장한다.

// 권장 방식
return Integer.compare(this.value, o.value);

구현 예시 (Integer 클래스 내부)

public final calss Integer extends Number implements Comparable {
	...
    public int compareTo(Integer anotherInteger) {
    	int v1 = this.value;
        int v2 = anotherInteger.value;
        
        // 같으면 0, 오른쪽 값이 크면 -1, 작으면 1을 반환
        return (v1 < v2) ? -1 : (v1 == v2 ? 0 : 1));
	...
}    

3. Comparator: "제 3자가 두 객체를 비교한다."

Comparable이 구현되어 있지 않거나, 이미 구현된 기준(오름차순) 말고 다른 기준(내림차순 등)으로 정렬하고 싶을 때 사용합니다.

인터페이스 정의

public interface Comparator<T> {
	// 제 3자의 입장에서 o1과 o2를 비교
    int compare(T o1, T o2);
    
    // (참고) equals는 모든 객체의 기본 메서드이므로 보통 구현 생략 가능
    boolean equals(Object obj);
}

구현 예시 (내림차순 정렬기)

기본 정렬 결과를 뒤집으려면 결과에 -1을 곱하거나, 파라미터의 순서를 바꿔서 비교하면 된다.

class Descending implements Comparator {
    public int compare(Object o1, Object o2) {
        if (o1 instanceof Comparable && o2 instanceof Comparable) {
            Comparable c1 = (Comparable)o1;
            Comparable c2 = (Comparable)o2;
            
            // -1을 곱해서 기본 정렬 방식(오름차순)을 뒤집는다.
            return c1.compareTo(c2) * -1; 
            // Tip: c2.compareTo(c1) 이렇게 순서를 바꿔도 내림차순이 된다.
        }
        return -1;
    }
}

4. 정렬 메서드 호출(Arrays.sort)

Arrays.sort()는 매개변수에 따라 적용되는 정렬 기준이 달라진다.

  • static void sort(Object[])
    • 객체 배열에 저장된 객체가 구현한 Comparable에 의한 정렬
  • static void sort(Object[] a, Comparator c)
    • 두번째 인자로 지정한 Comparator에 의한 정렬
class Main {
    public static void main(String[] args) throws IOException {
        String[] words = {"banana", "apple", "kiwi", "fig"};

        // Comparator를 람다식(Lambda)로 구현
        // 길이 오름차순 정렬
        Arrays.sort(words, (s1, s2) -> {
            return s1.length() - s2.length();
        });
        
        System.out.println(Arrays.toString(words));
        // ["apple", "banana", "fig", "kiwi"]
    }
}

5. 정렬의 원리: 기준과 로직의 분리

정렬의 기준만 제공하면 될까? 정렬 자체를 수행하는 코드는 어디에 있지?

정렬 알고리즘은 자리를 바꾸는 로직(Swap)자리를 바꿀지 결정하는 기준(Comparable)로 나뉜다.
아래 버블 정렬 예시를 보면, 정렬 알고리즘(For문)은 그대로인데 비교 로직(if문)만 갈아끼우는 것을 확인할 수 있다.

상황1: 객체의 기본 기준(Comparable) 사용

static void sort(Object[] objArr) {
    for (int i = 0; i < objArr.length - 1; i++) {
        for (int j = 0; j < objArr.length - 1 - i; j++) {
            // Comparable 형변환 후 compareTo 호출
            Comparable c = (Comparable) objArr[j];
            
            // "내가 쟤보다 크면(양수) 자리를 바꾼다" -> 오름차순
            if (c.compareTo(objArr[j+1]) > 0) { 
                Object tmp = objArr[j];
                objArr[j] = objArr[j+1];
                objArr[j+1] = tmp;
            }
        }
    }
}

상황 2: 외부 기준(Comparator) 사용

static void sort(Object[] objArr, Comparator c) {
    for (int i = 0; i < objArr.length - 1; i++) {
        for (int j = 0; j < objArr.length - 1 - i; j++) {
            // 외부 심판(Comparator)에게 비교를 위임
            // "심판이 보기에 앞이 뒤보다 크면(양수) 자리를 바꾼다"
            if (c.compare(objArr[j], objArr[j+1]) > 0) { 
                Object tmp = objArr[j];
                objArr[j] = objArr[j+1];
                objArr[j+1] = tmp;
            }
        }
    }
}   

6. 종합 예제 코드

class Ex11_7 {
	public static void main(String[] args) {
    	String[] strArr = {"cat", "Dog", "lion", "tiger"};
        
        // 1. Comparable에 의한 정렬 (기본: 사전순, 대문자 우선)
        Arrays.sort(strArr);
        System.out.println("strArr=" + Arrays.toString(strArr));
        // 결과: [Dog, cat, lion, tiger]
        
        // 2. Comparator에 의한 정렬 (특수: 대소문자 구분 없음)
        Arrays.sort(strArr, String.CASE_INSENSITIVE_ORDER);
        System.out.println("strArr=" + Arrays.toString(strArr));
        // 결과: [cat, Dog, lion, tiger]
        
        // 3. Comparator에 의한 정렬 (특수: 역순)
        Arrays.sort(strArr, new Descending());
        System.out.println("strArr=" + Arrays.toString(strArr));
        // 결과: [tiger, lion, cat, Dog]
    }   
}

7. 간단비교

  • Arrays.sort()
    • 배열 내에 Primitive Type을 정렬하기 위해 사용한다.
    • 기본 정렬은 오름차순이다.
  • Comprable
    • 데이터가 2개이상 들어있는 자료구조에서 정렬에 대한 기준을 마련하기 위해 사용한다.
    • 객체의 클래스 내 compareTo를 구현해 사용한다.
  • Comparator
    • 보통 Collections.sort의 2번째 인자로 사용된다.
    • 일시적으로 새로운 정렬기준을 사용할경우, 커스터마이징 된 새로운 정렬법을 정의하기 위해 사용된다.

📌 요약

  • Comparable: "나는 이렇게 정렬되는게 기본이야" (클래스 본연의 성질
  • Comparator: 이번만 이 기준으로 줄 서봐 (상황에 따른 전략)
  • 정렬 메서드: 너희가 정한 기준(compareTo/compare) 결과가 양수면, 나는 자리를 바꿔줄게

Reference

profile
Good Luck!

0개의 댓글