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

2022년 03월 16일

TOC

이 글은 Effective Java 3/E의 내용을 요약한 글입니다. 자세한 내용은 책을 참고하시기 바랍니다.

Comparable의 compareTo 메서드

  • compareTo는 Object의 메서드가 아닙니다.
  • Comparableequals와 유사하며, 동치성 비교 뿐만 아니라 순서 비교까지 한다는 특징과 제너릭하다는 특징이 있습니다.
  • 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입들은 Comparable을 구현하고 있다.
  • 만약 순서가 명확한 클래스를 작성하게 된다면 Comparable 인터페이스를 구현하여 정렬, 비교 등의 작업을 할 수 있습니다.

compareTo 메서드의 일반 규약

이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수로 반환한다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.

다음 설명에서 sgn(표현식) 표기는 수학에서 말하는 부호 함수(signum function)를 뜻하며, 표현식의 값이 음수, 0, 양수일 때 -1, 0, 1을 반환하도록 정의했다.

  • Comparable을 구현한 클래스는 모든 x, y에 대해 sgn(x.compartTo(y)) == -sgn.(y.compareTo(x))여야 한다(따라서 x.compareTo(y)는 y.compareTo(x)가 예외를 던질때에 한해 예외를 던져야 한다).
  • Comparable을 구현한 클래스는 추이성을 보장해야 한다. 즉, (x.compareTo(y) > 0 && y.compareTo(z) > 0)이면 x.compareTo(z) > 0이다.
  • Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))다.
  • 이번 권고가 필수는 아니지만 꼭 지키는 게 좋다. (x.compareTo(y) == 0) == (x.equals(y))여야 한다. Comparable을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다. 다음과 같이 명시하면 적당할 것이다.

"⚠️주의 : 이 클래스의 순서는 equals 메서드와 일관되지 않다."

CompareTo 규약 정리

  • compareTo는 타임이 다른 객체를 신경쓰지 않아도 되어 타입이 다른 객체가 주어진다면 ClassCastException을 던져도 된다.
  • compareTo 규약을 지키지 못한다면 비교를 활용하는 정렬된 컬렉션인 TreeSetTreeMap, Collecitons, Arrays와 어울리지 못합니다.
  • Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면 확장하는 대신 독립된 클래스를 만들고, 해당 클래스에는 원래 클래스의 인스턴스를 가르키는 필드를 두고, 내부 인스턴스를 반환하는 뷰 메서드를 제공해야한다.

위에 보인 compareTo의 규약들의 설명은 다음과 같다.

  • 첫번째 규약은 두 객체의 참조 순서를 바꿔 비교해도 예상한 결과가 나와야 한다는 뜻이다.
  • 두번째 규약은 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다는 뜻이다.
  • 세번째 규약은 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다는 뜻이다.
  • 네번째 규약은 compareTo메서드로 수행한 동치성 테스트 결과가 equals와 같아야 한다.

    • compareToequals의 결과가 일치하지 않아도 동작은 되지만 정렬된 컬렉션에 넣으면 엇박자를 낼 것이다.
    • 이유는, 기존에 컬렉션들은 equals규약을 따르지만 정렬된 컬렉션들은 equals가 아닌 compareTo를 사용하기 떄문이다.

compareTo 작성 요령

  • compareTo의 작성요령은 equals와 유사하다.
  • Comparable은 타입을 인수로 받는 제너릭 인터페이스라 compareTo의 인수 타입은 컴파일 타임에 정해진다.

    • 입력 인수 타입을 확인하거나 형 변환이 불필요하다.
    • null을 인수로 넣어 호출하면 NullPointException을 던져야 한다.
  • compareTo는 각 필드가 동치인지 비교하는 것이 아닌 순서를 비교하는 것이라 참조 필드를 비교하려면 compareTo를 재귀적으로 호출한다.

    • Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교할 경우에는 Comparator를 사용한다.
  • compareTo메서드에서 관계 연산자 <>를 사용하는 방식은 거추장스럽고 오류를 유발하여 추천하지 않는다.
  • 필드의 비교는 가장 핵심적인 필드부터 비교해가며 중간에 결과가 0(==)이 아니면 결과를 바로 반환하다.

비교자 생성 메서드 활용

자바 8에서는 Comparator 인터페이스가 비교자 생성 메서드(comparator construction method)와 팀을 꾸려 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다.

비교자 생성 메서드를 사용한 예시는 다음과 같습니다.

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

public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}
  • comparingInt는 객체 타입 참조를 int타입 키에 매핑하는 키 추출 함수를 인수로 받아 해당 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드이다.

    • 예제에서는 ((PhoneNumber pn) p -> pn.areaCode)와 같은 람다를 인수로 받고 Comparator<PhoneNumber>를 반환한다.
  • 자바의 타입 추론 능력은 강력하지 않기에 comparing((PhoneNumber pn) p> pn.areaCode)(PhoneNumber pn)와 같이 타입을 알려주는 것이 좋다.

    • 두번째 부터는 하지 않아도 된다.

주의 사항

값의 차이를 기준으로 값을 비교하는 compareTocompare메서드는 사용하면 안되는 방식이다. 이는 정수 오버플로우를 일으키거나 부동소수점 게산 방식에 따른 오류를 낼 수 있다.

// 값의 차를 기준으로 값을 비교하는 방식 (사용하면 안되는 예)
public class HashCodeOrderComparator {
    static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
        @Override
        public int compare(Object o1, Object o2) {
            return o1.hashCode() - o2.hashCode();
        }
    };
}

그 대신 아래의 두가지 방법을 활용하자

정적 compare 메서드를 활용한 비교자

class HashCodeOrder {
    static Comparator<Object> staticHashCodeOrder = new Comparator<Object>() {
        @Override
        public int compare(Object o1, Object o2) {
            return Integer.compare(o1.hashCode(), o2.hashCode());
        }
    };
}

비교자 생성 메서드를 활용한 비교자

class HashCodeOrder {
    static Comparator<Object> comparatorHashCodeOrder =
        Comparator.comparingInt(o -> o.hashCode());
}

핵심 정리

순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다. compareTo 메서드에서 필드의 값을 비교할 때 <> 연산자는 쓰지 말아야 한다. 그 대신 박싱된 기본 타입 클래스에서 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.

Buy me a coffeeBuy me a coffee
Written by

@Seongwon

기술공유를 통해 새로운 가치 창조을 추구하는 백엔드 개발자 오성원입니다.
©SeongwonOh