춤추는 개발자

[item 14] Comparable을 구현할지 고려하라 본문

Developer's_til/Effective Java

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

Heon_9u 2022. 10. 25. 19:17
728x90
반응형

 Comparable 인터페이스의 유일무이한 메서드인 compareTo를 알아보자. compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다. compareTo 메서드의 일반 규약은 equals의 규약과 비슷하다.

 

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

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

1. Comparable을 구현한 모든 클래스는 모든 x, y에 대해 sgn(x.compareTo(y)) == -sgn(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))다.
 - 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다는 뜻이다.

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

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

 

이상의 규약들은 compareTo 메서도르 수행하는 동치성 검사도 equals 규약과 똑같이 반사성, 대칭성, 추이성을 충족해야 함을 뜻한다.

 Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면, 확장하는 대신 독립된 클래스를 만들고, 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두자. 그런 다음 내부 인스턴스를 반환하는 '뷰' 메서드를 제공하면 된다. 이렇게 하면 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현해넣을 수 있다.

 

 compareTo 메서드는 각 필드가 동치인지를 비교하는게 아니라 그 순서를 비교한다. 다음 코드는 CaseInsensitiveString용 compareTo 메서드로, 자바가 제공하는 비교자를 사용하고 있다.  Compareble란 CaseInsensitiveString의 참조는  CaseInsensitiveString 참조와만 비교할 수 있다는 뜻이다.

public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
    public int compareTo(CaseInsensitiveString cis) {
        return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
    }

    // ...
}

 

 

 compareTo 메서드에서 관계 연산자 >, <를 사용하는 이전 방식은 추천하지 않는다. 다음은 PhoneNumber 클래스용 compareTo 메서드를 구현한 모습니다.

public int compareTo(PhoneNumber pn) {
    int result = Short.compare(areaCode, pn.areaCode);
    
    if(result == 0) {
        result = Short.compare(prefix, pn.prefix);
        if(result == 0) { 
            result = Short.compare(lineNum, pn.lineNum);
        }
    }
    
    return result;
}

 

 자바 8에서는 Comparator 인터페이스가 일련의 비교자 생성 메서드와 팀을 꾸려 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다. 이 비교자들을 Comparable 인터페이스가 원하는 compareTo 메서드를 구현하는데 멋지게 활용할 수 있다. 하지만, 약간의 성능 저하가 뒤따른다.

 대신 자바의 정적 임포트 기능을 이용하면 코드가 훨씬 깔끔해진다.

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

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

 

 이외에도 아래와 같이 '값의 차'를 기준으로 첫 번째 값이 두 번째 값보다 작으면 음수를, 두 값이 같으면 0을, 첫 번째 값이 크면 양수를 반환하는 compareTo나 compare 메서드와 마주할 것이다.

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

 

 이러한 방식은 사용하면 안된다. 정수 오버플로를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있다. 대신에 아래 두 가지 방식을 고려하자.

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());

 

 

728x90
반응형