개발 공부 기록하기/01. JAVA & Kotlin

짧은 코멘트와 함께하는 이펙티브 자바) #14 Comparable을 구현할지 고려하라

lannstark 2020. 10. 6. 20:21

짧은 코멘트

  1. Comparable을 생각보다 종종 구현을 하게 된다.
  2. 안타깝게 항상 헷갈리는 부분이 있는데 결과가 음수 혹은 양수일때 어떤 것이 앞에 가느냐... 이다 이럴때 해당 객체에 대한 단위 테스트를 깔끔하게 작성해주면 정말 좋다 👍

Comparable을 구현할지 고려하라

compareTo는 Object equals와 유사하다. 다른 점이라곤,

  • compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며 제네릭 한 것과
  • Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서가 있음을 뜻하는 것이다.

Comparable을 구현하면 검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게 할 수 있다. 자바 플랫폼 라이브러리의 모든 값 클래스와 열거타입은 Comparable을 구현했다.

Comparable의 일반 규약은 equals와 비슷하다

  • A.compareTo(B)는 A객체가 B객체보다 작으면 ‘음의 정수’ 같으면 ‘0’ 더 크면 ’양의 정수’를 반환해야 한다. 비교할 수 없다면 ClassCastException을 던진다.
  • sgn(x.compareTo(y)) = -sgn(y.compareTo(x))
  • x.compareTo(y) > 0 이고 y.compareTo(z) > 0이면 x.compareTo(z) 이다.
  • x.compareTo(y) == 0이면, sgn(x.compareTo(z)) == sgn(y.compareTo(z)) 이다.
  • 이번 권고는 필수가 아니지만 꼭 지키는 것이 좋다. (x.compareTo(y) == 0) == (x.equals(y)) 만약 이 권고를 지키지 않는다면 그 사실을 명시해야 한다.

2번째부터 4번째까지 세 규약은 compareTo가 equals와 똑같이 반사성, 대칭성, 추이성을 충족해야 함을 뜻한다. 그래서 주의사항도 같다. 기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가했다면 compareTo 규약을 지킬 방법이 없다. 우회법도 같다. 상속대신 합성을 사용하면 된다.

compareTo의 마지막 규약은 필수가 아니지만 꼭 지키길 권한다.

(compareTo가 ’같다’라고 판단했으면 equals가 true여야 한다) > 정렬된 컬렉션들은 동치성을 비교할 때 equals 대신 compareTo를 사용하기 때문이다.

예를 들어 compareTo와 equals가 일관되지 않은 BigDecimal을 예로 들어 생각해보자. BigDecimal("1.0")BigDecimal("1.00")을 HashSet에 넣게 되면 HashSet은 equals를 사용하여 2개의 원소를 갖게 된다. 하지만 HashSet 대신 TreeSet을 사용하면 compareTo가 사용되어 1개의 원소를 갖게 된다.

 

compareTo를 구현할 때에는 관계 연산자인 > 와 < 를 사용하기 보다 박싱된 기본 타입 클래스들의 새로 추가된 정적 메소드를 사용하는 것이 좋다.

또한 성능을 높이기 위해서는 가장 핵심적인 필드부터 비교해 나가야 한다.

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

Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 Comparator를 대신 사용하면 된다.

JAVA 8에서는 Comparator 인터페이스가 일련의 비교 생성 메소드(comparator construction method)와 팀을 꾸려 메소드 연쇄 방식으로 비교자 생성을 할 수 있게 되었다. 약간의 성능 저하가 있지만 훨씬 깔끔해진다.

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);
}

comparingInt는 람다를 인수로 받으며, 이 람다는 PhoneNumber에서 추출한 지역 코드를 기준으로 전화번호의 순서를 정하는 Comparator를 반환한다.

만약 객체의 hashCode를 기준으로 값을 비교하고 싶다면 o1.hashCode() - o2.hashCode()라는 위험한 코드 대신 Integer.compare나 Comparator의 comparingInt를 사용하는 것이 좋다.

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());