본문 바로가기

IT/자바

[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 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.

반응형