스터디/이펙티브 자바

[Effective Java] Item14. Comparable을 구현할지 고려

📝 작성 : 2022.05.21  ⏱ 수정 : 
728x90

Comparable와 compareTo

Comparable는 compareTo 메서드를 하나만 갖고있는 있는 인터페이스입니다. compareTo 메서드는 두 가지 성격만 빼면 eqauls와 같습니다.
compareTo는 동치성 비교 뿐아니라 순서까지 비교할 수 있으며 제네릭합니다.

compareTo의 일반 규약

  1. 두 객체 참조의 순서를 바꿔도 같은 결과가 나와야 합니다.
  2. a<b, b<c 이면 a<c이어야 합니다.
  3. 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 합니다.
  4. (권장사항) 객체 x,y에 대하여 x.compareTo(y) == 0 이면 x.eqauls(y)여야 합니다.

compareTo는 equals와는 다르게 다른 타입의 객체를 신경쓰지 않아도됩니다. 대부분은 ClassCastException 예외를 던집니다. 물론, 다른 타입 사이의 비교도 허용하지만, 보통은 비교 대상들이 구현한 공통 인터페이스를 매개로 작동합니다.
compareTo 규약을 지키지 못하면 비교를 활용하는 클래스(TreeSet, TreeMap, Collections, Arrays 등)와 어울리지 못합니다.
마지막 사항은 권장사항이지만 이를 지키지 않으면 컬렉션에 넣었을 때 제대로 작동하지 않을 수 있습니다.

compareTo 구현

compareTo 메서드는 각 필드가 동치인지를 비교하는 것이 아니라 순서를 비교합니다. 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출합니다.

Comparable를 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 comparator를 대신 사용합니다.

객체 참조 필드가 하나뿐인 경우

public class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
    private final String s;

    public CaseInsensitiveString(String s) {
        this.s = Objects.requireNonNull(s);
    }

    @Override
    public int compareTo(CaseInsensitiveString cis) {
        return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
    }
}

Comparable<CaseInsensitiveString>를 구현함으로써 CaseInsensitiveString끼리만 비교할 수 있습니다.

기본 타입 필드가 여럿인 경우

public class PhoneNumber implements Comparable<PhoneNumber>{
    private int areaCode;
    private int prefix;
    private int lineNum;

    @Override
    public int compareTo(PhoneNumber pn) {
        int result = Integer.compare(areaCode, pn.areaCode); //가장 중요한 필드
        if (result != 0) return result;

        result = Integer.compare(prefix, pn.prefix); //두 번째로 중요한 필드
        if (result != 0) return result;

        result = Integer.compare(lineNum, pn.lineNum); //세 번째로 중요한 필드
        return result;
    }
}

비교자 생성 메서드를 활용하는 경우

자바 8에서는 Comparator 인터페이스를 이용하여 연쇄방식으로 비교자를 생성할 수 있게 되었습니다. 간결하지만 약간의 성능저하가 있습니다.

public class PhoneNumber implements Comparable<PhoneNumber>{
    private int areaCode;
    private int prefix;
    private int lineNum;

    @Override
    public int compareTo(PhoneNumber pn) {
        return COMPARATOR.compare(this, pn);
    }

    private static final Comparator<PhoneNumber> COMPARATOR =
            Comparator.comparingInt((PhoneNumber pn) -> pn.areaCode)
                    .thenComparingInt(pn -> pn.prefix)
                    .thenComparingInt(pn -> pn.lineNum);

}

해시코드 값의 차를 기준으로하는 경우 - 추이성을 위배!

이 방식은 사용하면 안됩니다. 정수 오버플로우를 일으키거나 IEEE 754 부동소수점 계산 방식에 따른 오류를 낼 수 있습니다.

static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return o1.hashCode() - o2.hashCode();
    }
};

다시한번 이 방법은 사용하면 안됩니다. 대신 아래의 두 방법 중 하나를 선택해서 사용합니다..

정적 compare 메서드를 활용하는 경우

static Comparator<Object> hashCodeOrder = new Comparator<>() {
    public int compare(Object o1, Object o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};

비교자 생성 메서드를 활용하는 경우

static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

정리

순서를 고려해야 한다면 꼭 Comparable을 구현하여 컬렉션이 제공하는 정렬, 검색, 비교 기능을 잘 사용할 수 있도록 해야합니다.
compareTo 메서드에서 필드를 비교할 때 <, > 연산자를 사용하지 않습니다. 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 ComparaTor 인터페이스가 제공하는 비교자 생성 메서드를 사용합니다.

반응형