아이템 12. Comparable 구현을 고려하라.

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예외를 던진다.

  1. compareTo를 구현할 때는 모든 x와 y에 대해 sgn(x.spmpareTo(y)) == -sgn(y.compareTo(x))가 만족되도록 해야 한다. y.compareTo(x)가 예외를 발생시킨다면, x.compareTo(y)도 그래야 하고, 그 역도 성립해야 한다.)
  2. compareTo를 구현할 때는 추이성(transitivity)이 만족되도록 해야 한다. 즉, (x.compareTo(y) > 0 && y.compareTo(z) > 0)이면, x.compareTo(z) > 0이어야 한다.
  3. x.compareTo(y) == 0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))의 관계가 모든 z에 대해 성립하도록 해야 한다.
  4. 강력히 추천하는 조건 하나는 (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)되어 음수가 되어 버린다.