ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 모든 객체의 공통 메서드 - 아이템 14. Comparable을 구현할지 고려하라
    Study/Effective Java 2022. 12. 15. 14:17

    Comparable을 구현할지 고려하라

    Comparable의 compareTo 메서드

    compareTo() 메서드는 Object의 메서드가 아니다. 특징 두 가지만 빼면 Object의 equals() 메서드와 동일하다. 그렇다면 무엇이 다를까? compareTo() 메서드는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다. Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서가 있음을 뜻한다. 그래서 Comparable을 구현한 객체들의 배열은 손쉽게 정렬이 가능하다.

    사실상 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이 Comparable을 구현했다. 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하는 것이 좋다.

     

    compareTo 메서드의 일반 규약

    compareTo() 메서드의 일반 규약은 Object의 equals() 메서드의 규약과 비슷하다.

     

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

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

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

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

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

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

     

    모든 객체에 대해 전역 동치 관계를 부여하는 equals() 메서드와 달리, compareTo() 메서드는 타입이 다른 객체를 신경 쓰지 않아도 된다. 타입이 다른 객체가 주어지면 간단히 ClassCastException을 던져도 되며, 대부분 그렇게 한다.

     

    물론, 이 규약에서는 다른 타입 사이의 비교도 허용하는데, 보통은 비교할 객체들이 구현한 공통 인터페이스를 매개로 이루어진다.

     

    hashCode 규약을 지키지 못하면 해시를 사용하는 클래스와 어울리지 못하듯이, compareTo 규약을 지키지 못하면 비교를 활용하는 클래스와 어울리지 못한다. 비교를 활용하는 클래스의 예로는 정렬된 컬렉션인 TreeSet과 TreeMap, 검색과 정렬 알고리즘을 활용하는 유틸리티 클래스인 Collections와 Arrays가 있다.

     

    위에서 언급한 compareTo() 메서드의 일반 규약에 대해 좀 더 자세히 살펴보겠다.

     

    1. 첫 번째 규약의 경우 두 객체의 참조 순서를 바꾸어 비교해도 예상한 결과가 나와야 한다는 의미이다.

    2. 두 번째 규약은 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다는 뜻이다.

    3. 마지막 규약은 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다는 뜻이다.

     

    위의 세 규약은 comapreTo() 메스드로 수행하는 동치성 검사도 equals()  메서드의 규약과 마찬가지로 반사성, 대칭성, 추이성을 충족해야 함을 뜻한다.

     

    comapreTo 작성 요령

    compareTo() 메서드의 작성 요령은 equals() 메서드와 유사하다. 몇 가지 차이점만 주의하면 된다.

     

    1. Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로, compareTo() 메서드의 인수 타입은 컴파일 타임에 정해진다. 따라서, 입력 인수의 타입을 확인하거나 형 변환할 필요가 없다.

     

    2. 객체 참조 필드를 비교하려면 compareTo() 메서드를 재귀적으로 호출한다.

     

    3. Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 비교자(Comparator)를 대신 사용한다.

     

    4. compareTo() 메서드에서 관계 연산자 '<'와 '>'를 사용하는 이전 방식은 거추장스럽고 오류를 유발하니, 박싱된 기본 타입 클래스들의 정적 메서드인 compare()을 사용한다.

     

    5. 클래스에 핵심 필드가 여러 개라면, 가장 핵심적인 필드부터 비교해 나간다.

     

    비교자 생성 메서드를 이용한 비교자
    private static final Comparator<PhoneNumber> COMPARATOR = 
                    comparingInt((PhoneNumber pn) -> pn.areaCode)
                            .thenComparingInt(pn -> pn.prefix)
                            .thenComparingInt(pn -> pn.lineNum);
    
    @Override
    public int compareTo(PhoneNumber pn) {
        return COMPARATOR.compare(this, pn);
    }

    위 코드는 클래스를 초기화할 때 비교자 생성 메서드 2개를 이용해 비교자를 생성한다.

    첫 번째인 comparingtInt() 메서드는 객체 참조를 int 타입 키에 매핑하는 키 추출 함수를 인수로 받아, 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드다.

    두 번째 비교자 생성 메서드인 thenComparingInt() 메서드 Comparator의 인스턴스 메서드로, int 키 추출자 함수를 입력받아 다시 비교자를 반환한다. 이 비교자는 첫 번째 비교자를 적용한 다음 새로 추출한 키로 추가 비교를 수행한다. thenComparingint() 메서드는 원하는 만큼 연달아 호출할 수 있다.

    자바의 타입 추론 능력이 최초의 comparingInt() 메서드를 호출할 때에는 타입을 알아낼 만큼 강력하지 않기 때문에, 입력 인수의 타입인 phoneNumber를 명시해 주었지만, 그 이후에 thenComparingInt를 호출할 때는 충분히 타입 추론이 가능하므로 명시하지 않았다.

     

    주의 사항
    static Comparator<Object> hashCoderOrder = new Comparator<>() {
        public int compare(Object o1, Object o2) {
            return o1.hashCode() - o2.hashCode();
        }
    };

    가끔씩, '값의 차'를 기준으로 첫 번째 값이 두 번째 값보다 작으면 음수를, 두 값이 같으면 0을, 첫 번째 값이 크면 양수를 반환하는 compareTo()나 compare() 메서드를 찾아볼 수 있다. 위 소스코드가 그 예이다.

    위 방식은 사용해서는 안 되는 방식이다. 이 방식은 정수 오버플로우를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있다. 그렇다고 월등히 빠른 방식도 아니다.

     

    그 대신 아래의 두 방식 중 하나를 사용하는 것이 좋다.

     

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

    위 소스코드는 정적 compare()  메서드를 활용한 비교자이다.

     

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

    위 소스코드는 비교자 생성 메서드를 활용한 비교자이다.

     

    최종 정리

    순서를 고려해야 하는 값 클래스를 작성해야 한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다.

    compareTo() 메서드에서 필드의 값을 비교할 때 '<'와 '>' 연산자는 쓰지 말아야 한다. 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare() 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.


    출처

     이펙티브 자바 Effective Java 3/E. 조슈아 블로크 저자(글) · 개앞맵시(이복연) 번역

     

    728x90

    댓글

Designed by Tistory.