자바에서 compareTo가 의미하는 것

1. 자바에서 compareTo란?

compareTo 메서드는 Comparable 인터페이스가 제공하는 메서드로 두 객체를 비교할 때 사용하는 메서드입니다.

public class Jewel implements Comparable<Jewel> {
        int weight;
        int value;

        public Jewel(int weight, int value) {
            this.weight = weight;
            this.value = value;
        }

        @Override
        public int compareTo(Jewel o) {
            return this.weight - o.weight;
        }
}

 

다음과 같이, Comparable 인터페이스를 구현한 객체는 compareTo 메서드를 오버라이딩 해야 합니다. 이 때, 해당 메서드를 호출하는 쪽(this)과 호출받는 쪽(o)의 값을 비교하여 양수, 0, 음수 중 한 가지를 반환합니다. (반환형 int 타입)

 

그렇다면, compareTo가 반환하는 값의 의미는 무엇일까요? 코드와 함께 알아보도록 하겠습니다!

 

@Test
void compareToTest() {
    //given
    Integer a = 1;
    Integer b = 3;

    System.out.println(a.compareTo(b)); // -1
}

 

다음과 같이, 1의 입장에서 3과 비교하는 compareTo를 호출하면, 음수(-1)를 반환합니다. compareTo를 사칙연산의 마이너스라고 생각해보면 이 값의 의미를 빠르게 이해할 수 있습니다.

  • 1.compareTo(3) => 1 - 3 < 0

 따라서 호출하는 쪽이 값이 크다면 양수(1)가 반환되고, 같다면 (0)이 반환됩니다.

 

compareTo는 compare를 호출합니다. 따라서 비교하는 두 값의 차이에 관계없이 대소 관계에 따라 1, 0, -1 중 하나만 반환합니다.

정리

  1. 내가 더 크다면? compareTo는 1(양수)을 반환
  2. 내가 더 작다면? compareTo는 -1(음수)을 반환
  3. 둘이 같다면? compareTo는 0을 반환

2. 자바에서 객체 정렬을 하는 방법

자바는 원시 타입(int, char 등)의 경우, DualPivotQuicksort.sort()를 참조 타입(Integer, String 등)의 경우, TimSort.sort()를 사용한다고 합니다.

https://brandpark.github.io/java/2021/01/05/arrays_sort1.html

 

[ JAVA ] Arrays.sort()의 내부 동작(1) - 자바니또의 Tech선물

개요 알고리즘 공부를 하다 Arrays.sort()와 Collections.sort()의 내부는 어떤 정렬을 사용하는지 궁금해졌다. 공부한 결과부터 말하자면 Arrays.sort는 인자의 타입이 원시타입(PrimitiveType)인 경우에는 DualP

brandpark.github.io

객체 정렬을 다루는 만큼, Timsort.sort() 코드를 기준으로 자바에서 정렬을 어떤 기준으로 어떻게 하는지를 빠르게 정리해보겠습니다.

1) 자바는 오름차순으로 정렬한다

TimeSort.sort의 일부

TImeSort 객체를 생성하고 정렬에 필요한 요소 개수(nRemaining)만큼 정렬을 하는데, 이때, countRunAndMakeAscending 메서드를 호출합니다. Ascending이라는 키워드를 통해 대충이라도 짐작할 수 있을 것입니다.

 

 

해당 코드의 구현부가 잘 이해가 되지 않다면, 주석만 보고 전체적인 흐름만 이해하셔도 충분합니다.

먼저, c.compare(a[runHi], a[runHi - 1]) < 0을 보면, 두 요소의 값을 비교합니다. 이 때, runHi번째 요소가 runHi - 1번째 요소보다 작을 때 reverseRange를 합니다.

 

c.compare(a[runHi], a[runHi - 1])는 a[runHi].compareTo(a[runHi - 1])와 같다고 생각하면 됩니다. 즉, c.compare(a[runHi], a[runHi - 1]) < 0은 runHi가 값이 2라면, 2번째 요소가 1번째 요소보다 작을 때를 의미합니다. 작은 값이 뒷 쪽에 있으니 내림차순이라는 걸 알 수 있습니다.

 

따라서 TimSort는 배열이 내림차순이면 오름차순으로 정렬하고, 오름차순이라면 그대로 오름차순 상태를 유지합니다.

 

이 때, 우리가 주목해야 할 부분은 else절입니다. c.compare(a[runHi], a[runHi - 1]) >= 0 즉, a[runHi].compareTo(a[runHi - 1]) >= 0이라면, 오름차순 상태를 유지합니다. compareTo 값이 양수라면, 이를 호출하는 자가 뒷 쪽에 있다는 것을 알 수 있습니다.

 

2) 객체를 정렬할 때, 어떤 값을 반환할 것인가

우리는 앞서, 정렬할 때, 오름차순으로 값이 정렬된다는 것을 알았습니다. 이 원리를 활용하면 특정 값을 기준으로 객체를 오름차순 혹은 내림차순으로 정렬할 수 있습니다.

[1] 오름차순으로 정렬하고 싶다면?

sort 메서드에 의해 객체가 정렬될 때, 재정의한 compareTo가 호출될 것입니다. 이 때, 내가 비교되는 대상보다 특정 값이 더 클 때, 뒤로 가야 한다면 비교한 compareTo 값이 양수를 반환해야 합니다.

public class Jewel implements Comparable<Jewel> {
    int weight;

    public Jewel(int weight) {
        this.weight = weight;
    }

    @Override
    public int compareTo(Jewel o) {
        if (this.weight > o.weight) {
            return 1;
        } else if (this.weight < o.weight) {
            return -1;
        } else {
            return 0;
        }
    }
}

@Test
void sort메서드() {
    //given
    Jewel jewel1 = new Jewel(1);
    Jewel jewel2 = new Jewel(3);
    Jewel jewel3 = new Jewel(2);

    //when
    List<Jewel> jewels = new ArrayList<>();
    jewels.add(jewel1);
    jewels.add(jewel2);
    jewels.add(jewel3);

    for (Jewel jewel : jewels) {
        System.out.println(jewel.weight);
    }

    //then
    Collections.sort(jewels);
    System.out.println("===========정렬 후===========");
    for (Jewel jewel : jewels) {
        System.out.println(jewel.weight);
    }
}

 

compareTo를 호출했을 때, 비교대상(o)와의 특정 값(weight)을 비교하여 내가 더 값이 크면 양수를 반환하고, 오름차순 형태를 유지합니다. 반대로 내가 작다면 음수를 반환해서 앞 순서로 이동하여 마찬가지로 오름차순 상태를 유지합니다.

 

@Override
public int compareTo(Jewel o) {
    if (this.weight > o.weight) {
        return 1;
    } else if (this.weight < o.weight) {
        return -1;
    } else {
        return 0;
    }
}

@Override
public int compareTo(Jewel o) {
    return Integer.compare(this.weight, o.weight);
}

 

다중 if문은 다음과 같이 Integer.compare 혹은 this.weight - o.weight으로 간단히 표현할 수 있습니다. 만약, 내가 더 크다면 해당 값들은 반드시 양수를 반환하기 때문이죠.

[2] 내림차순으로 정렬하고 싶다면?

오름차순의 메커니즘을 이해했다면, 내림차순은 바로 이해할 수 있습니다. 내가 정렬되어야 하는 상태에서 compareTo가 호출되고, 내가 비교대상(o)보다 크다면 앞으로 이동해야 합니다. 이는 반대로 말하면 나보다 작은 친구가 뒤로 가야함을 의미하죠.

 

@Override
public int compareTo(Jewel o) {
    if (this.weight < o.weight) {
        return 1;
    } else if (this.weight > o.weight) {
        return -1;
    } else {
        return 0;
    }
}

@Override
public int compareTo(Jewel o) {
    return Integer.compare(o.weight, this.weight);
}

 

따라서 다음과 같이, 표현할 수 있습니다. 내가 값이 더 클 때, 앞으로 이동하려면 음수 값을 반환해야 하기 때문입니다. sort 메서드는 객체의 값들을 직접 비교하지 않으며, compareTo의 반환 값으로 판단합니다. 따라서 우리는 특정 값을 기준으로 원하는 조건에 맞게 양수, 음수, 0을 반환해야 합니다.

 

정리

이제 우리는 객체 정렬을 하기 위해 compareTo를 오버라이딩 할 때, 다음과 같이 생각하면 됩니다.

sort 메서드는 정렬하고자 하는 객체의 compareTo를 호출한다. compareTo 값이 양수일 경우, 오름차순을 유지한다. 따라서 특정 기준을 만족할 때, 뒷 쪽으로 이동하려면 양수를 반환하도록 해야 한다.