이펙티브 자바 - item 14. Comparable을 구현할지 고려하라

item 14. Comparable을 구현할지 고려하라

이펙티브 자바 - item 14. Comparable을 구현할지 고려하라

모든 구체 클래스에서 Object의 toString을 재정의하자.

compareTo 메서드

  • 단순 동치성 비교 + 순서까지 비교
  • 제네릭
  • Comparable을 구현한 객체의 배열은 손쉽게 정렬이 가능하다.
  • 자바 플랫폼 라이브러리의 모든 값 클래스, 열거 타입은 모두 Compareable을 구현하고 있다.

compareTo 메서드의 일반 규약

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

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

  1. Comparable을 구현한 클래스는 모든 x, y에 대해 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))여야 한다(따라서 x.compareTo(y)는 y.compareTo(x)가 예외를 던질 때에 한해 예외를 던져야한다.

  2. Comparable을 구현한 클래스는 추이성을 보장해야 한다. 즉, (x.compareTo(y) > 0 && y.compareTo(z) > 0)이면 x.compareTo(z) > 0이다.

  3. Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))이다.

  4. 이번 권고가 필수는 아니지만 꼭 지키는 게 좋다. (x.compareTo(y) == 0) == (x.equals(y))여야 한다. Comparable을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다. 다음과 같이 명시하면 적당할 것이다.

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

  • 1, 2, 3 : compareTo 메서드로 수행하는 동치성 검사도 equals 규약과 같이 반사성, 대칭성, 추이성을 충족해야한다.

  • 4 : compareTo 메서드로 수행한 동치성테스트의 결과가 equals와 같아야한다.
    • 필수는 아니지만 지키지 않는다면 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스에 정의한 동작과 엇박자를 낼 수 있다. ( 지키지 못한다면 명시해주어야 한다.)
      • 정렬된 컬렉션(TreeSet 등)은 equals가 아닌 compareTo를 사용해 동치성을 비교하기 떄문
  • equals()와 같이 상속을 사용해 새로운 값을 추가하면 규약을 지킬 방법이 없다.
  • equals()와 같이 상속이 아닌 컴포지션을 사용하면 이 문제점은 해결할 수 있다.

작성 요령

  • 타입을 인수로 받는 제네릭 인터페이스이므로 컴파일 시 인수타입은 정해진다.
  • 동치인지를 비교하는 게 아니라 순서를 비교한다.
  • compareTo()에서 관계연산자 <, >를 사용하는 이전 방식은 사용하지 말자.
  • 핵심 필드가 여러 개이면 어느 것을 먼저 비교하느냐에 따라 중요해진다. 따라서 가장 핵심적인 필드부터 먼저 비교하자.

객체 참조 필다가 하나뿐인 비교자

public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
		public int compareTo(CaseInsensitiveString cis) {
				return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s) // 자바에서 제공하는 비교자를 사용
}

객체 참조 필드가 여러개인 비교자

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

    @Override
    public int compareTo(PhoneNumber phoneNumber) {
        int result = Short.compare(areaCode, phoneNumber.areaCode);   // 가장 중요한 필드
        if (result == 0) {
            result = Short.compare(prefix, phoneNumber.prefix);       // 두 번째로 중요한 필드
            if (result == 0) {
                result = Short.compare(lineNum, phoneNumber.lineNum); // 세 번째로 중요한 필드

                .....

            }
        }
        return result;
    }
}

주의

혹시나 ‘-‘ 연산자를 사용해 값의 차’를 기준으로 음수나 양수를 반환하는 compareTo나 compare 메서드를 구현하려는 시도를 할 수도 있다.

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

예를 들어 위의 코드와 같이 값의 차이를 이용하는 방식은 정수 오버플로우를 일으키거나, 부동소수점 계산 시 오류를 일으킬 수 있다.

따라서 위의 방식 대신 아래의 2가지 방식을 사용해 compareTo나 compare 메서드를 구현해야 한다.

정적 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 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야한다.

대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자