[Effective Java] Item 14 Comparable을 구현할지 고려하라
Comparable의 compareTo는 Object의 equals와 동일하지만 2가지 다른점이 존재한다.
1. 동시성 비교 + 순서 비교
2. 제네릭 형태
Comparable을 구현한 객체는 순서가 존재함으로 Arrays.sort로 쉽게 정렬할 수 있다.
ex) 명령줄 인수들을 중복 제거 후 알파벳 순으로 정렬
public static void main(String[] args){
Set<String> s = new TreeSet<>(); //TreeSet (순서 정렬 + 중복제거)
Collections.addAll(s, args);
System.out.println(s);
}
String이 Comparatable을 구현되어있으므로 가능한 코드이다.
순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.
Comparable의 CompareTo 메서드의 일반 규약은 Equals 규약과 비슷하다.
1. 반사성 : A>B = B<A
2. 추이성 : A<B, B<C 이면, A<C 이다
3. 일관성 : A = B 이면, A와 C 비교결과 = B와 C 비교결과 같다
4. compareTo 결과가 equals 결과와 같아야 한다 (필수 조건 X, 지키길 권장)
- 위 조건을 충족하지 않더라도 동작에는 문제가 없다. 하지만 Collection, Set, Map과 같은 정렬 컬렉션에 넣으면 정의된 동작과는 다른 결과를 얻게 된다.
예를 들어 compareTo와 equals이 다른 BigDecimal 클래스를 본다면,
HashSet 인스턴스(=equals 메소드)에 new BigDecimal("1.0"), new BigDecimal("1.00")을 넣으면 원소를 2개 가지게 됨
반면에 TreeSet 인스턴스(=compareTo 메소드)에 new BigDecimal("1.0"), new BigDecimal("1.00")을 넣으면 원소를 1개 가지게 됨
compareTo 메서드 작성 요령은 다음과 같다.
1) Comparable 타입을 인수로 받는 제네릭 인터페이스임으로 CompareTo 메소드의 인수 타입은 컴파일때 정해진다.
-> 타입을 잘못 정의하면 컴파일 에러 발생, null을 넘겨줄 경우 nullPointerException 발생
2) 각 필드가 동치인지가 아닌 순서를 비교한다.
-> 비교자는 직접 만들거나 자바에서 제공하는 것 중에 골라서 사용하면 된다.
관계 연산자인 <, >을 사용하는 것보다 Double.compare, Float.compare를 사용하는걸 권한다.
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString>{
public int compareTo(CaseInsentiveString cis){
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
...
}
3) 비교해야되는 핵심필드가 여러개라면 우선순위에 따라 순차적으로 비교하도록 구현한다.
public int compareTo(PhoneNumber pn){
int result = Short.compare(areaCode, pn.areaCode); //가장 중요한 지역번호
if (result == 0) {
result = Shore.compare(prefix, pn.prfix); // 두번째 중요한 앞 번호
if (result == 0) {
result = Shore.compare(lineNum, pn.lineNum); // 세번째 중요한 뒷 번호
}
}
return result;
}
자바8에서는 비교 생성 메서드를 통해 메서드 연쇄 방식으로 간결하게 구현할 수 있다.
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prfix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn){
return COMPARATOR.compare(this, pn);
}
이 방식은 간결하지만 성능 이슈가 발생한다.
아래와 같이 hashcode의 차를 통해 비교할 수 있다.
static Comparator<Object> hashCodeOrder = new Comparator<>(){
public int compare(Object o1, Object o2){
return o1.hashCode() - o2.hashCode();
}
}
하지만 이 방식은 사용하면 안된다.
왜냐하면 정수 오버플로우를 발생시키거나 IEEE 754 부동소수점 계산 방식에 따른 오류가 발생할 수 있기 때문이다.
대신 아래의 방식을 사용하길 권한다.
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 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.