Java

[Effective Java] 아이템14. Comparable을 구현할지 고려하라

jun9.com 2022. 4. 1. 03:58

요약

  • Comprable 인터페이스
  • compareTo 구현방법
  • Comprator를 활용한 메서드 연쇄

 


1. 순서를 고려해야 하는 클래스라면 Comparable을 구현해야한다.

public interface Comparable<T> {
    public int compareTo(T o); // Comparable의 유일한 메서드
}

[Comprable의 특징]

  • compareTo는 동치성을 비교한다는 점에서 equals와 성격이 비슷하다.
  • 다만 equals와 다르게 순서가 반영되어 있다.

 

-> Comparable을 구현한다는 것은 그 클래스의 인스턴스들에는 순서가 있음을 뜻한다.

 

따라서, 알파벳, 숫자, 연대와 같이 순서가 명확한 값 클래스를 작성한다면, Comparable 인터페이스를 구현하자!!

 

 

 

[Comparable 구현 이점]

  • Comparable을 구현하여 컬렉션을 더 잘 활용할 수 있다.

배열은 Arrays.sort(a); 와 같은 코드로 정렬할 수 있다.

Collection을 활용하여 순서값을 정렬할 수 있다.

public class WordList{
    public static void main(String[] args){
        Set<String> s = new TreeSet<>();
        Collections.addAll(s, args);
        System.out.println(s);// 알파벳 순서대로 출력
    }
}

 

 


compareTo() 일반규약

- 이 객체가 주어진 객체보다 작으면 음의 정수(-1), 크다면 양의 정수(1)를 반환한다.

- 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.

- -1,0,1을 반환한다.

 

1. 대칭성을 만족한다. 두 객체를 바꿔 비교해도 예상한 결과가 나와야 한다. 

sgn(x.compareTo(y)) == -sgn(y.compareTo(x))

 

2. 추이성을 보장해야 한다.

(x.compareTo(y) > 0 && y.compareTo(z) >0)라면, x.compareTo(z) > 0

 

3. 크기가 같은 객체들은 어떤 객체와 비교하더라도 항상 같아야 한다.

x.compareTo(y) == 0이면(=x와 y의 차이가 같다면) sgn(x.compareTo(z)) == sgn(y.comareTo(z))

 

4. (SHOULD) (x.compareTo(y) == 0) == (x.equals(y))

필수는 아니지만 지키는게 좋음. 지키지 않을 시에는 그 사실을 주석으로 명시해야 한다. (예시: 이 클래스의 순서는 equals 메서드와 같은 결과를 보장하지 않는다 = 동치성을 보장하지 않는다.) 

 

 

*) Collection에 동치성를 만족하지 않는 객체를 넣는다면, Collection의 equals와 다른 동작을 한다!

Collection이 동치성을 비교할 때  내부적으로 compareTo를 따르기 때문이다.

 

[예시]

new BigDecimal("1.0");
new BigDecimal("1.00");

 

를 HashSet에 넣으면 원소 2개 -> equals로 비교하기 때문에

를 TreeSet에 넣으면 원소 1개 -> compareTo로 비교하기 때문에

 


 

2. compareTo 작성 요령

1. 기본적으로 박싱된 기본 타입 클래스에서 제공하는 정적 메서드 compare을 사용하여 비교한다. 

ex: Short.compare(), Integer.compare()...

(<,>로 직접 비교하는 것은 오류를 유발하여 자바 7부터는 추천하지 않는다.)

 

2. 핵심필드가 여러개라면 가장 핵심적인 필드부터 비교해라. (비교결과가 0이 아니라면 거기서 비교가 끝나기 때문)

 

[정적 compare 메서드를 활용]

public int compareTo(PhoneNumber pn){
	int result = Short.compare(areaCode, pn.areaCode); //가장 중요한 필드
    if(result == 0){
    	result = String.compare(prefix, pn.prefix); // 두 번째로 중요한 필드
        if(result == 0){
        	result = Integer.compare(lineNume, pn.lineNum);// 세 번째로 중요한 필드
        }
    }
    return result;
}

 

 


3. Interface Comparator (java8)

- Comparator를 사용하여 비교를 더 다양하게 구현할 수 있다.

- 메서드 연쇄방식으로 비교자를 생성할 수 있다!!

- 간결하지만 약간의 성능 저하가 나타난다. 

 


Comparator은 다양한 유틸성 비교 메서드를 제공한다.

 public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
    }

Comparator를 반환하기 때문에 연쇄방식으로 작성할 수 있다.

 

 

[비교자 생성 메서드 활용 - 메서드 연쇄 가능]

private static final Comparator<PhoneNumber> COMPARATOR = 
	comparingInt((PhoneNumber pn) -> pn.areaCode()) //지역코드를 기준으로 전화번호의 순서를 정하는 Comparator<PhoneNumber>을 반환
    		.thenComparingInt(pn -> pn.prefix))
            .thenComparingInt(pn -> pn.lineNum);
            
            
 public int compareTo(PhoneNumber pn){
 	return COMPARATOR.compare(this, pn);
 }

4. 정리

[BAD] 해시코드 값의 차를 기준으로 비교 -> 추이성 위배]

static Comparator<Object> hashCodeOrder = new Comparator<>(){
	public int compare(Object o1, Object o2){
    	return o1.hashCode() - o2.hashCode();
    }
}

정수 오버 플로우나 부동소수점 계산 방식에 따른 오류가 발생할 수 있다.

참고) 부동소수점 계산 방식: https://codetorial.net/articles/floating_point.html

 

[GOOD] 정적 compare 메서드를 활용한 비교자

static Comprator<Object> hashCodeOrder = new Comparator<>(){
	public int compare(Object o1, Object o2){
    	return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};

 

[GOOD] 비교자 생성 메서드를 활용한 비교자

static Comparator<Object> hashCodeOrder = 
	Comparator.comparingInt(o -> o.hashCode());