Comparable을 구현하면 검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게 할 수 있다.
예컨대 다음 코드는 명령줄 인수들을 알파벳순으로 출력한다. String이 Comparable을 구현한 덕분이다.
publicclassWordList {
publicstaticvoidmain(String[] args) {
// TreeSet은 순서가 존재하는 정렬된 컬렉션으로 compareTo를 통해 비교, 정렬을 수행한다.Set<String> s = newTreeSet<>();
Collections.addAll(s, args); // Collection s에 args의 원소를 하나씩 추가한다.System.out.println(s);
}
}
Comparable을 구현하면 이 인터페이스를 활용하는 수많은 제네릭 알고리즘과 컬렉션의 힘을 누릴 수 있다.
사실상 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이 Comparable을 구현했다.
알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.
compareTo의 일반 규약
아래의 규약은 compareTo 메소드로 수행하는 동치성 검사도 equals 규약과 마찬가지로 반사성, 대칭성, 추이성을 충족해야함을 의미한다.
이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수를,
같으면 0을, 크면 양의 정수를 반환한다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면
ClassCastException을 던진다.
다음 설명에서 sgn(표현식) 표기는 수학에서 말하는 부호 함수(signed 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 메소드와 일관되지 않다."
타입이 다른 객체가 주어지면 ClassCastException을 던져도 된다. 물론 이 규약에서는 다른 타입 사이의 비교도 허용한다. (보통은 비교할 객체들이 구현한 공통 인터페이스를 매개로 비교가 이루어진다.)
비교를 활용하는 클래스의 예로는 정렬된 컬렉션인 TreeSet과 TreeMap, 검색과 정렬 알고리즘을 활용하는 유틸리티 클래스인 Collections와 Arrays가 있다.
확장 클래스에서 compareTo 규약을 지키는 방법
기존 클래스를 확장한 구체 클래스에서 새로운 값 필드를 추가했다면 compareTo 규약을 지킬 방법이 없다. 이 때는 equals 때의 해결 방법과 마찬가지로 확장하는 대신 독립된 클래스를 만들고 이 클래스에 기존 클래스의 인스턴스를 가리키는 필드를 두는 식으로 구성하여 compareTo 규약을 지킬 수 있다. (독립된 클래스에서 Comparable을 구현)
compareTo와 equals의 동치성 검사 결과는 같아야 한다.
compareTo의 순서와 equals의 결과가 일관되지 않은 클래스는 동작은 하지만, 이 클래스의 객체를 정렬된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스(Collection, Set, 혹은 Map)에 정의된 동작과 엇박자를 낼 것이다.
이 인터페이스들은 equals 메소드의 규약을 따른다고 되어 있지만 놀랍게도 정렬된 컬렉션들은 동치성을 비교할 때 equals 대신 compareTo를 사용하기 때문이다. 따라서 이 문제를 주의해야 한다.
/** * 예제: compareTo와 equals의 동치성 검사가 일관되지 않은 BigDecimal 클래스 */Set<BigDecimal> set;
BigDecimaldecimal1 = newBigDecimal("1.0");
BigDecimaldecimal2 = newBigDecimal("1.00");
// equals로 비교하기 때문에 다른 객체로 인식하여 원소가 2개로 잡힌다.set = newHashSet<>();
set.add(decimal1);
set.add(decimal2);
assertEquals(2, set.size());
// compareTo로 비교하기 때문에 같은 객체로 인식하여 원소가 1개로 잡힌다.set = newTreeSet<>();
set.add(decimal1);
set.add(decimal2);
assertEquals(1, set.size());
compareTo 메소드 작성 시 주의사항
1. compareTo 메소드의 인수 타입은 컴파일 타임에 정해진다.
Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메소드의 인수 타입은 컴파일 타임에 정해진다.
이는 입력 인수의 타입을 확인하거나 형변환할 필요가 없다는 뜻이다. 만약 인수의 타입이 잘못됐다면 컴파일 자체가 되지 않는다. 또한 null을 인수로 넣어 호출하면 NPE가 던져질 것이다.
2. compareTo 메소드는 각 필드가 동치인지를 비교하는 게 아니라 그 순서를 비교한다.
객체 참조 필드를 비교하려면 compareTo 메소드를 재귀적으로 호출한다.
Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 비교자(Comparator)를 대신 사용한다. 비교자는 직접 만들거나 자바가 제공하는 것 중에 골라쓰면 된다.
compareTo에서 관계 연산자(<, >)를 사용하는 이전 방식은 거추장스럽고 오류를 유발하니 사용하지 마라. Java 7부터 박싱된 기본 타입 클래스들에 추가된 정적 메소드 compare를 이용해 비교를 수행하라.
4. 핵심 필드가 여러 개라면 가장 핵심적인 필드부터 비교하라.
클래스에 핵심 필드가 여러 개라면 가장 핵심적인 필드부터 비교하면 된다. 비교 결과가 0이 아니라면 그 결과를 반환하고, 같다면 똑같지 않은 필드를 찾을 때까지 그 다음으로 중요한 필드를 비교하면 된다.
@OverridepublicintcompareTo(PhoneNumberpn) {
intresult = Short.compare(areaCode, pn.areaCode); // 1번째로 중요한 필드if (result == 0) {
result = Short.compare(prefix, pn.prefix); // 2번째로 중요한 필드if (result == 0)
result = Short.compare(lineNum, pn.lineNum);// 3번째로 중요한 필드
}
returnresult;
}
5. 값의 차를 기준으로 하는 비교자는 사용하지 마라.
값의 차를 기준으로 하는 비교자는 정수 오버플로우를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있어 compareTo 규약의 추이성을 위배하게 되므로 사용해서는 안된다.
Java 8에서는 Comparator 인터페이스가 일련의 비교자 생성 메소드(comparator construction method)를 이용해 메소드 연쇄 방식으로 비교자를 생성할 수 있게 되었다. 그리고 이 비교자들을 이용해 Comparable 인터페이스가 원하는 compareTo 메소드를 구현하는 데 멋지게 활용할 수 있다.
/** * comparingInt에서 생성한 Comparator로 비교한 후 결과 값이 0이라면 * 새로운 key 추출 함수를 이용해 Comparator를 만들고 이를 이용해 추가 비교를 수행한다. */Comparator<PhoneNumber> comparator =
Comparator.comparingInt((PhoneNumberpn) -> pn.getAreaCode())
.thenComparingInt(pn -> pn.getPrefix())
.thenComparingInt(pn -> pn.getLineNum());
비교자 생성 메소드 : comparingLong, comparingDouble
이외에도 long과 double용 비교자 생성 메소드로 comparingLong 과 comparingDouble 를 제공한다.
short처럼 더 작은 정수 타입에는 int용 버전을 사용하고, float은 double용을 이용해 비교를 수행한다.
비교자 생성 메소드 : comparing
객체 참조용 비교자 생성 메소드도 제공한다.
앞에서와 마찬가지로 부차 순서를 정하기 위한 thenComparing 메소드도 제공한다. (설명 생략)
// 1. 객체의 키 추출자를 받아 비교자를 반환한다.publicstatic <T, UextendsComparable<? superU>> Comparator<T> comparing(
Function<? superT, ? extendsU> keyExtractor) { ... }
// 2. 키 추출자 하나와 추출된 키를 비교할 비교자까지 총 2개의 인수를 받아// keyExtractor로 추출한 데이터를 keyComparator를 통해 비교하는 Comparator를 반환한다.publicstatic <T, U> Comparator<T> comparing(
Function<? superT, ? extendsU> keyExtractor,
Comparator<? superU> keyComparator);