Comparable을 구현할지 고려하라
compareTo 메서드
compareTo는 Object의 메서드가 아니다. compartTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다. Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서가 있음을 뜻한다. 그래서 Comparable을 구현한 객체들의 배열은 Arrays.sort(a)
와 같이 손쉽게 정렬할 수 있다.
자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이 Comparable을 구현했다. 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.
compareTo 메서드 일반규약
이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수, 같으면 0, 크면 양의 정수를 반환한다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.
-
Comparable을 구현한 클래스는 모든 x, y에 대해
sgn(x.compareTo(y)) == -sgn(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을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다.
ex) 이 클래스의 순서는 equals 메서드와 일관되지 않다.
모든 객체에 대해 전역 동치관계를 부여하는 equals 메서드와 달리, compareTo는 타입이 다른 객체를 신경쓰지 않아도 된다. 타입이 다른 객체가 주어지면 간단히 ClassCastException을 던져도 되며, 대부분 그렇게 한다. 물론, 이 규약에서는 다른 타입 사이의 비교도 허용하는데, 보통은 비교할 객체들이 구현한 공통 인터페이스를 매개로 이뤄진다.
compareTo 규약을 지키지 않으면 비교를 활용하는 클래스인 TreeSet, TreeMap, 검색과 정렬 알고리즘을 활용하는 유틸리티 클래스인 Collections, Arrays와 어울리지 못한다.
compareTo 메서드로 수행하는 동치성 검사도 equals 규약과 똑같이 반사성, 대칭성, 추이성을 충족해야 한다. 주의사항도 똑같다. 기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가했다면 객체 지향적 추상화의 이점을 포기할 생각이 아니라면 compareTo 규약을 지킬 방법이 없다.
Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면, 확장하는 대신 독립된 클래스를 만들고, 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두자. 그런 다음 인스턴스를 반환하는 뷰 메서드를 제공하면 된다.
compareTo의 순서와 equals의 결과가 일관되지 않아도 클래스는 동작한다. 단, 이 클래스의 객체를 정렬된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스(Collection, Set, Map)에 정의된 동작과 엇박자를 낼 것이다. 이 인터페이스들은 equals 메서드의 규약을 따른다고 되어 있지만, 정렬된 컬렉션들은 동치성을 비교할 때 equals 대신 compareTo를 사용한다.
BigDecimal 클래스 예
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("1.00");
System.out.println(b1.equals(b2)); // false
System.out.println(b1.compareTo(b2)); // 0
Set<BigDecimal> hashSet = new HashSet<>();
hashSet.add(b1);
hashSet.add(b2);
System.out.println(hashSet.size()); // 2
Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(b1);
treeSet.add(b2);
System.out.println(treeSet.size()); // 1
compareTo 메서드 작성요령
인수 타입과 Null
Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메서드의 인수 타입은 컴파일 타임에 정해진다. 따라서 입력 인수의 타입을 확인하거나 형변환할 필요가 없다. 인수 타입이 잘못되면 컴파일 자체가 되지 않는다. 또한 null을 인수로 넣어 호출하면 NullPointException을 던져야 한다.
Comparator
compareTo 메서드는 각 필드가 동치인지를 비교하는 게 아니라 그 순서를 비교한다. 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출한다. Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 Comparator를 대신 사용한다. Comparator는 직접 만들거나 자바가 제공하는 것 중에 골라 쓰면 된다.
public final class CaseInsenstiveString implements Comparable<CaseInsenstiveString> {
public int compareTo(CaseInsenstiveString cis) {
return String.CASE_INSENSTIVE_ORDER.compare(s, cis.s);
}
...
}
기본 타입 필드 비교
compareTo 메서드에서 관계 연산자 <와>를 사용하는 이전 방식은 거추장스럽고 오류를 유발하니 박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare를 이용하면 된다.와>
Short.compare(x, y);
Integer.compare(x, y);
Double.compare(d1, d2);
핵심 필드가 여러개인 경우
필드가 여러 개인 경우 어느 것을 먼저 비교하느냐가 중요해진다. 가장 핵심적인 필드부터 비교하자. 가장 핵심 필드가 똑같다면, 똑같지 않은 필드를 찾을 때까지 그 다음으로 중요한 필드를 비교해나간다.
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);
}
}
자바 8에서는 Comparator 인터페이스가 일련의 비교자 생성 메서드와 팀을 꾸려 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다. 그리고 이 비교자들은 Comparable 인터페이스가 원하는 compareTo 메서드를 구현하는 데 멋지게 활용할 수 있다. 하지만 약간의 성능 저하가 뒤따른다.
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
위 코드는 클래스를 초기화할 때 비교자 생성 메서드 2개를 이용해 비교자를 생성한다. 첫 번째인 comparingInt는 객체 참조를 int 타입 키에 매핑하는 키 추출함수를 인수로 받아, 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드다.
두 전화번호의 지역 코드가 같은 경우 두 번째 비교자 생성 메서드인 thenComparingInt가 수행한다. thenComparingInt는 Comparator의 인스턴스 메서드로, int 키 추출자 함수를 입력받아 다시 비교자를 반환한다.
thenComparingInt를 호출할 때 타입을 명시하지 않은 이유는 자바의 타입 추론 능력이 이 정도는 추론해낼 수 있기 때문이다.
비교자(Comparator) 생성시 주의사항
해시코드 값의 차를 기준으로 하는 비교자
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
}
위 방식은 정수 오버플로를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있다. 그렇다고 이번 아이템에서 설명한 방법대로 구현한 코드보다 월등히 빠르지도 않을 것이다. 아래 두 방식 중 하나를 사용하자.
정적 compare 메서드를 활용한 비교자
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());
}
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스를 쉽게 정렬하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다. compareTo 메서드에서 필드의 값을 비교할 때 <와> 연산자는 쓰지 말아야 한다. 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.와>
Comments