12. Comparable 구현을 고려하라.
Comparable 인터페이스를 구현하는 클래스의 객체들은 자연적 순서(natural ordering)를 갖게 된다. Comparable을 구현한 객체들의 배열을 정렬하는 것은 아래와 같이 아주 간단하다.
Arrays.sort(a);
또한, Comparable인터페이스를 구현한 객체들은 검색하거나 최대/최소치를 계산하기도 간단하며, 그 컬렉션을 정렬된 상태로 유지하기도 쉽다. 그리고, 자바 플랫폼 라이브러리에 포함된 거의 모든 값 클래스(value class)는 Comparable 인터페이스를 구현 한다. 알파벳 순서나 값의 크기, 또는 시간적 선후관계처럼 명확한 자연적 순서를 따르는 값 클래스를 구현할 때는 Comparable 인터페이스를 구현할 것을 반드시 고려해 봐야 한다.
public interface Comparable<T>{
int cpmpareTo(T t);
}
Comparable 구현 클래스(값 클래스)는 주어진 객체와 비교를 하는데 인자 값으로
주어진 객체보다 작으면 -1,
같으면 0,
크면 1를 반환한다.
인자로 전달된 객체의 자료형이 이 객체와 비교 불가능한 자료형인 경우에는 ClassCastException
예외를 던진다.
- compareTo를 구현할 때는 모든 x와 y에 대해
sgn(x.spmpareTo(y)) == -sgn(y.compareTo(x))
가 만족되도록 해야 한다.y.compareTo(x)
가 예외를 발생시킨다면,x.compareTo(y)
도 그래야 하고, 그 역도 성립해야 한다.) - compareTo를 구현할 때는 추이성(transitivity)이 만족되도록 해야 한다.
즉,
(x.compareTo(y) > 0 && y.compareTo(z) > 0)
이면,x.compareTo(z) > 0
이어야 한다. x.compareTo(y) == 0
이면sgn(x.compareTo(z)) == sgn(y.compareTo(z))
의 관계가 모든 z에 대해 성립하도록 해야 한다.- 강력히 추천하는 조건 하나는
(x.compareTo(y) == 0) == (x.equals(y))
이다. 일반적으로 CompareTo인터페이스를 구현하면서 이 조건을 만족하지 않는 클래스는 반드시 그 사실을 명시해야 한다.
4번 사항에 대해 적절한 예로 BigDecimal 클래스가 있다. 이 클래스의 compareTo메서드는 equals에 부합하지 않는다. HashSet객체를 만들고 new BigDecimal(“1.0”)과 BigDecimal(“1.00”)으로 만든 객체를 추가하면, 두 객체가 추가 된다. 이 두 객체를 equals로 비교하면 서로 다르다고 판단하기 때문이다. 하지만, TreeSet을 사용하면 집합에는 하나의 객체만 삽입된다. compareTo로 비교하면 두 객체는 같은 객체이기 때문이다. (TreeSet은 순서를 갖는 객체(compareTo로 비교)이고, HashSet은 중복을 제거해주는 객체(equals로 비교)하는 객체이다)
비교할 필드가 Comparable을 구현하지 않고 있거나 특정상황에서 좀 특이한 순서 관계(자연적 순서가 아닌 경우)를 사용해야 할 경우에는 Comparator를 명시적으로 사용하면 된다. Comparator는 직접 작성할 수도 있고, 아래의 compareTo메서드에서 처럼 이미 있는 Comparator를 사용할 수도 있다.
public final class CaseInsensitiveString implements Compareable<CaseInsensitiveString>{
public int compareTo(CaseInsensitiveString cis){
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
}
정수형의 기본 자료형 필드는 관계 연산자 (relational operator) < 와 >를 사용해 비교한다.
public int compareTo(PhoneNumber pn){
// 지역 번호 비교
if( areaCode < pn.areaCode ) return -1;
if( areaCode > pn.areaCode ) return 1;
// 국번 비교
if( prefix < pn.prefix ) return -1;
if( prefix > pn.prefix ) return 1;
// 회선 번호 비교
if( lineNumber < pn.lineNumber ) return -1;
if( lineNumber > pn.lineNumber ) return 1;
// 모든 필드 일치
return 0;
}
정수형 필드의 경우 단순히 부호로 비교하는 것 보다 크기로 비교하면 더 효율적이다.
public int compareTo(PhoneNumber pn){
// 지역 번호 비교
int areaCodeDiff = areaCode - pn.areaCode
if( 0 != areaCodeDiff ) return areaCodeDiff;
// 국번 비교
int prefixDiff = prefix - pn.prefix
if( 0 != prefixDiff ) return prefixDiff;
// 회선 번호 비교
int lineNumberDiff = lineNumber - pn.lineNumber
if( 0 != lineNumberDiff ) return lineNumberDiff;
// 모든 필드 일치
return 0;
}
[주의] 위 기법은 비교하려는 정수가 음수가 아닐때만 사용할 수 있다. 정확히 말해서, 필드 값의 최소치와 최대치가 Integer.MAX_VALUE(2^31 -1)이 하인 경우에만 사용할수 있다. 부호 비트가 있는 32비트 정수 안에, 비호 비트가 있는 32 비트 정수 두개의 차이를 온전히 담을 수 없기 때문이다. i가 큰 int 값이고, j가 큰 음수 int값일 때, i - j는 오버플로(overflow)되어 음수가 되어 버린다.