반응형

ITEM 14 "Comparable 을 구현할지 고려하라"

 

Comparable 인터페이스에는 compareTo 메서드가 있다.

얼핏 보면 Object 의 equals 메서드와 동일해 보이는데 단순 동치성 비교를 넘어서서 순서까지 비교할 수 있고 제네릭하다는 특징이 있다. 그래서 Comparable 인터페이스를 구현한 클래스들은 자연적인 순서(natural order)가 있다.

 

public class WordList {
    public static void main(String[] args) {
        Set<String> s = new TreeSet<>();
        Collections.addAll(s, args);
        System.out.println(s);
    }
}

 

이 예제는 TreeSet 에 명령 인자들을 추가하는데 출력했을 때 알파벳순으로 출력된다. String 클래스 자체가 Comparable 인터페이스를 구현한 덕분이다. 이처럼 자바 플랫폼 라이브러리에 있는 값 클래스와 열거 타입이 Comparable 을 구현하고 있어 손쉽게 정렬을 할 수 있다.

 

Comparable 의 compareTo 메서드도 equals 와 유사한 규약을 가지고 있다.

 

compareTo 메서드 일반 규약

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

 

대칭성

sgn(x.compareTo(y)) == -sgn(y.compareTo(x))

-> 여기서 sgn 은 부호함수로 표현식이 음수면 -1, 0 이면 0, 양수이면 1을 반환한다.

추이성

x.compareTo(y) > 0 이고 y.compareTo(z) > 0 이면 x.compareTo(z) > 0

반사성

x.compareTo(y) == 0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))

equals 와 동치 (권장사항)

(x.compareTo(y) == 0) == (x.equals(y))

 

hashCode 규약을 지키지 못하면 해시를 사용하는 클래스와 어울리지 못하듯, compareTo 규약을 지키지 못하면 비교를 활용하는 클래스들과 어울리지 못 한다. TreeSet 과 TreeMap 같은 정렬이 보장되어 있는 컬렉션들, 유틸리티 클래스인 Collections 와 Array 도 compareTo 규약을 지켜야만 제대로 동작한다.

 

마지막 4번째 규약은 꼭 지키라고 권장하고 있다.

빈 HashSet 인스턴스를 생성한 다음 new BigDecimal("1.0") 과 new BigDecimal("1.00") 을 차례대로 추가했다고 가정해보자. 이 두 BigDecimal 을 equals 메서드로 비교하면 서로 다르기 때문에 HashSet 은 2 개의 원소를 가진다. 하지만 TreeSet 에 담았다면 하나의 원소를 가지게 된다. equals 결과값과 compareTo 결과값이 다르면 나중에 컬렉션을 옮길 때 문제가 발생할 수 있다.

 

compareTo 메서드 올바르게 사용하기

 

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

 

Comparable 인터페이스는 제네릭 인터페이스이므로 인수 타입이 컴파일타임에 정해진다. 타입을 확인하거나 형변환할 필요가 없다. 인수의 타입이 잘못되었다면 컴파일 자체가 안 되어 Object 처럼 형변환을 안 해도 된다.

 

자바 7 이전에는 정수 기본 타입을 비교할 때 <, > 연산자를 사용하고 실수 기본 타입 필드를 비교할 때 정적 메서드인 Double.compare 와 Float.compare 을 사용하라고 권고했었다. 그런데 자바 7 도입 이후 박싱 타입 클래스에서 기본 타입에 대한 정적 메서드를 제공하고 있어 그냥 compare 로만 비교하면 된다.

 

위 예제에서 비교할 필드가 여러 개라면, 가장 핵심이 되는 필드부터 비교해나가자.

비교 결과가 0 이 아니라면 순서가 결정되었으니 바로 리턴하고, 같다면 그 다음 주요 필드를 계속해서 비교하면 된다.

 

자바 8에서는 Comparator 인터페이스가 비교자 생성 메서드를 제공하고 있다.

약간의 성능 저하가 있지만, 함수형 프로그래밍처럼 메서드 연쇄 호출을 할 수 있어 아래와 같이 코딩하기도 한다.

 

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

 

이해 안 되는 부분이지만,,,

솔직히 마지막은 이해가 안 된다. hashcode 함수를 거친 결과 값들이 입력 값의 순서대로 증감을 가진다면 의미가 있겠지만, 내가 알기로는 해시함수가 순서를 보장해주지 않는다. 책에서 소개한 것처럼, hashcode 를 비교할 일은 없을 것 같다.

 

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

 

여담이지만 그래도 진짜 이 코드가 무슨 의미일까 한참을 생각했다. (Object 의 hashCode 는 순서를 보장해주나?)

hashCode 를 비교하기 보다는 "-" 에 의미를 맞춰 소개한 것 같다. - 를 사용하면 Int 의 경우 정수 오버플로우를 일으킬 가능성이 있고 Float 의 경우 부동소수점 계산 방식에 따른 오류가 발생할 수 있다.

 

static Comparator<Object> hashCodeOrder = new Comparator<>() {
	(Object o1, Object o2) -> Integer.compare(o1.hashCode(), o2.hashCode())
    }
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

 

- 비교연산자를 사용할 바에는 정적 compare 메서드를 활용하거나 비교자 생성 메서드를 활용하자.

 

"순서를 고려해야 하는 값 클래스를 작성한다면 Comparable 인터페이스를 구현하라"

 

반응형

+ Recent posts