본문 바로가기
책/effective java

[effective java] item 14. Comparable을 구현할지 고려하라

by 2245 2023. 8. 1.

결론

순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다.
compareTo 메서드에서 필드의 값을 비교할 때 < 와  > 연산자는 쓰지 말아야 한다.
'값의 차'를 기준으로 하는 Comparator도 좋지 않다.  
그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.

 

정적 compare 메서드를 활용한 Comparator 

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

 

비교자 생성 메서드를 활용한 Comparator

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

public int compareTo(Object o) {
	return hashCodeOrder.compare(this, o);
}

 

 

설명

Comparable 인터페이스 

public interface Comparable<T> {
    public int compareTo(T o);
}

compareTo는 앞서 재정의하라는 메서드들과 다르게 Object의 메서드가 아닙니다.

하지만 Object의 equals와 두 가지 성격만 빼면 같습니다.

 

  1.  compareTo는 동치성 비교에 더하여 순서까지 비교할 수 있다.
  2. 제네릭하다.

 

Comparable을 구현한다는 의미는 그 클래스의 인스턴스들에는 자연적인 순서(natural order)가 있음을 뜻합니다. 

따라서, Comparable을 구현한 객체들의 배열은 다음처럼 손쉽게 정렬할 수 있습니다.

 

Arrays.sort(a);

 

또한, 검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게 할 수 있습니다.

명령줄 인수들의 중복을 제거하고, 알파벳 순으로 출력

public class WordList {
	public static void main(String[] args) {	//String이 Comparable을 구현한 덕분입니다. 
    	Set<String> s = new TreeSet<>();
        Collections.addAll(s, args);
        Sysetm.out.println(s);
    }
}

 

 

 

CompareTo 일반 규약

compareTo 메서드의 일반 규약은 equals 규약과 비슷합니다.

참고) 규약들 중 너무 당연한 이야기는 생략하겠습니다. (p. 87 ~ 94) 
equals 규약과 똑같이 반사성, 대칭성, 추이성을 충족해야 한다는 규약입니다. 

 

자신 객체와 주어진 객체의 순서를 비교합니다. 
자신 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환합니다. (오름차순 정렬 기준)
이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던집니다. 
  • (생략)
  • ...
  • 이번 권고는 필수는 아니지만 꼭 지키는 게 좋다.
    (x.compareTo(y) == 0) == (x.equals(y))여야 한다.
    Comparable을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다. 다음과 같이 명시하면 적당할 것이다.
    "주의: 이 클래스의 순서는 equals 메서드와 일관되지 않다."

 

 

마지막 규약: compareTo 의 결과가 equals와 같아야 한다.

마지막 규약은 필수는 아니지만 꼭 지키길 권합니다.

간단히 말하면 compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다는 것입니다.

 

compareTo 순서와 equals의 결과가 일관되지 않아도 동작은 합니다. 

단, 이 클래스의 객체를 정렬된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스(Collection, Set, Map) 에 정의된 동작과 엇박자를 내게 됩니다. 

이 인터페이스들은 equals 메서드의 규약을 따른다고 되어 있지만, 놀랍게도 정렬된 컬렉션들은 동치성을 비교할 때 equals 대신 compareTo를 사용하기 때문입니다.

아주 큰 문제는 아니지만, 주의해야 합니다.

 

compareTo와 equals가 일관되지 않는 BigDecimal 

public static void main(String[] args) {
    BigDecimal val1 = new BigDecimal("1.0");
    BigDecimal val2 = new BigDecimal("1.00");

    System.out.println(val1.equals(val2));		//false

    Set<BigDecimal> hset = new HashSet<>();		
    hset.add(val1);
    hset.add(val2);

    System.out.println(hset.size());		//2 (hashSet은 equals 메서드로 비교)

    Set<BigDecimal> tset = new TreeSet<>();
    tset.add(val1);
    tset.add(val2);

    System.out.println(tset.size());		//1	(treeSet은 compareTo 메서드로 비교)
}
  • BigDecimal 객체 두 개를 생성합니다.
  • 두 객체를 equals 메서드로 비교하면 서로 다르기 때문에 false를 반환합니다. 
  • HashSet은 두 객체를 equals 메서드로 비교하기 때문에 2개의 원소를 갖게 됩니다.
  • 반면에, TreeSet은 compareTo 메서드로 비교하는데, compareTo로 비교하면 두 인스턴스가 똑같기 때문에 1개의 원소를 갖게 됩니다. 

 

 

참고 정적 메서드 compare
compareTo 메서드에서 정수 기본 타입 필드를 비교할 때, 관계 연산자 < 와 > 보다, 박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare 을 사용하는 것을 권장합니다.
ex)
Short.compare(areaCode, o.areaCode);

 

 

비교자 생성 메서드를 활용한 Comparator 

  • 자바 8에서 Comparator 인터페이스가 일련의 비교자 생성 메서드(comparator construction method)와 팀을 꾸려 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었습니다. 
  • 간결함때문에 많은 프로그래머가 매혹되지만, 약간의 성능 저하가 뒤따릅니다. (필자의 컴퓨터에서 10%정도 느려졌습니다.)
  • 자바의 정적 임포트 기능을 이용하면 정적 비교자 생성 메서드들을 그 이름만으로 사용할 수 있어 코드가 훨씬 깔끔해집니다.

 

PhoneNumber 클래스 예제

private static final Comparator<PhoneNumber> COMPARATOR = comparingInt((PhoneNumber pn) -> pn.areaCode)
				.thenComparingInt(pn -> pn.prefix)
				.thenComparingInt(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
	return COMPARATOR.compare(this, pn);
}
  • 클래스를 초기화할 때 Comparator 생성 메서드 2개를 이용해 Comparator를 생성합니다.
  • 그 첫 번째인 comparingInt는 객체 참조를 int 타입 키에 매핑하는 키 추출 함수(key extractor function)를 인수로 받아, 그 키를 키준으로 순서를 정하는 Comparator를 반환하는 정적메서드입니다.
  • 현재 예시에서 comparingInt는 람다(lamda)를 인수로 받으며, 이 람다는 PhoneNumber에서 추출한 지역 코드를 기준으로 전화번호의 순서를 정하는 Comprator<PhoneNumber>를 반환합니다.
  • 이 람다에서 입력 인수의 타입(PhoneNumber pn)을 명시한 점을 주목해야 합니다.
  • 자바의 타입 추론 능력이 이 상황에서 타입을 알아낼 만큼 강력하지 않기 때문에 프로그램이 컴파일되도록 우리가 도와준 것입니다.
  • 두 전화번호의 지역 코드가 같을 수 있으니 비교 방식을 더 다듬어야 합니다.
  • 이 일은 두 번째 비교자 생성 메서드인 thenComparingInt가 수행합니다.
  • thenComparingInt는 Comparator의 인스턴스 메서드로, int 키 추출자 함수를 입력받아 다시 Comparator를 반환합니다. (이 Comparator는 첫 번째 Comparator를 적용한 다음 새로 추출한 키로 추가 비교를 수행합니다.)
  • thenComparingInt는 연달아 호출할 수 있습니다. 
  • 이번에는 thencomparingInt를 호출 할때 타입을 명시하지 않았습니다. 자바의 타입 추론 능력이 이 정도는 추론해낼 수 있기 때문입니다. 

 

 

객체 참조용 Comparator 생성 메서드

추가 공부 필요..

 

 

 

값의 차를 기준으로 하는 Comparator - 권장하지 않는다

이따금 '값의 차'를 기준으로 첫 번째 값이 두 번째 값보다 작으면 음수를, 두 값이 같으면 0을, 첫 번째 값이 크면 양수를 반환하는 compateTo나 compare 메서드를 볼 수 있습니다. 

해시코드 값의 차를 기준으로 하는 Comparator - 추이성을 위배한다. 

static Comparator<Object> hashCodeOrder = new Comparator<>() {
	public int compare(Object o1, Object o2) {
    	return o1.hashCode() - o2.hashCode();
    }
}
  • 이 방식은 사용하면 안됩니다.
  • 이 방식은 정수 오버플로를 일으키거나 IEEE 754 부동소수점 계산방식에 따른 오류를 낼 수 있습니다.
  • 그렇다고 다른 방식들보다 월등히 빠르지도 않습니다.

 

 

대신 다음의 두 방식 중 하나를 사용하는 것을 권장합니다. 

 

정적 compare 메서드를 활용한 Comparator 

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

 

비교자 생성 메서드를 활용한 Comparator

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

 

 

참고 equals와 마찬가지로 기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가한 경우 compareTo 규약을 지킬 방법이 없다. 
단, 객체 지향적 추상화의 이점을 포기하지 않는 한 말입니다. (아이템 10). 우회법도 equals와 동일하게 컴포지션을 사용하는 방법이 있습니다.
(Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면, 확장하는 대신 독립된 클래스를 만들고, 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 둔다. 그런 다음 내부 인스턴스를 반환하는 '뷰' 메서드를 제공한다. 이렇게 하면 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현해넣을 수 있다. 클라이언트는 필요에 따라 바깥 클래스의 인스턴스를 필드 안에 담긴 원래 클래스의 인스턴스로 다룰 수도 있다. )

 

 


출처

https://www.yes24.com/Product/Goods/65551284

 

이펙티브 자바 Effective Java 3/E - 예스24

자바 플랫폼 모범 사례 완벽 가이드 - Java 7, 8, 9 대응자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브

www.yes24.com