주니어 개발자 성장기

아이템 14. Comparable을 구현할지 고려하라 본문

Java/이펙티브 자바

아이템 14. Comparable을 구현할지 고려하라

Junpyo Lee 2023. 9. 9. 16:22

개요

Object에 포함되는 인터페이스는 아니지만, 널리 사용될 수 있는 인터페이스이다.

  • Object.equals에 더해서 순서까지 비교할 수 있으며 Generic을 지원한다.

 

CompareTo규약

  1. 반사성
  2. 대칭성
  3. 추이성
  4. 일관성

 

 

권장

compareTo가 0을 반환한다면 equals도 true를 반환하도록 구현하는 것이 좋다. BigDecimal 클래스의 경우 equalscompareTo의 결과가 다를 수도 있도록 구현되었다.

BigDecimal oneZero = new BigDecimal("1.0");
BigDecimal oneZeroZero = new BigDecimal("1.00");
System.out.println(oneZero.compareTo(oneZeroZero)); // Tree, TreeMap
System.out.println(oneZero.equals(oneZeroZero)); // 순서가 없는 콜렉션

결과(출력)

0
false

 

 

 

구현방법

Comparable<T>를 구현하게 되면 T 타입과 비교할 수 있는 compareTo(T t) 메서드를 오버라이딩해야한다.

  1. 기본타입의 경우 박싱타입의 compare 메서드를 호출해서 구현해주자.

 

 

 

유의점

  • 상속을 할 경우 equals의 경우처럼 규약을 지키는 것과 다형성을 사용하는 것 중 양자택일을 해야한다.따라서 이런 경우에는 컴포지션을 쓰는 것을 책에서 권장한다.
  • 자바 8부터는 Comparator 인터페이스의 static 메서드를 활용해 Comparator 인스턴스의 생성이 가능하다. 이를 통해 가독성이 좋은 compareTo를 만들 수 있다.
// 코드 14-3 비교자 생성 메서드를 활용한 비교자 (92쪽)
private static final Comparator<PhoneNumber> COMPARATOR =
        comparingInt((PhoneNumber pn) -> pn.areaCode)
                .thenComparingInt(pn -> pn.getPrefix())
                .thenComparingInt(pn -> pn.lineNum);

@Override
public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}

위 코드는 Comparator 를 static import 했다는 것에 주의하자. 성능이 10% 정도 느리다고 하지만, Comparator가 엄청 자주 쓰이는 것이 아니라면 크게 개의치 않아도 된다.

 

 

 

그 외

Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메서드의 인수 타입은 컴파일타임에 정해진다. 입력 인수의 타입을 확인하거나 형변환할 필요가 없다는 뜻이다. - 이펙티브 자바 3판 90p

Comparable이 제네릭 인터페이스이므로 매개 변수가 옳은 지를 컴파일 타임에 체크가 가능하다는 장점이 있다는 뜻이다.

자바의 타입 추론 능력이 이 상황에서 타입을 알아낼 만큼 강력하지 않기 때문에 프로그램이 컴파일되도록 우리가 도와준 것이다. - 이펙티브 자바 3판 92p

// 코드 14-3 비교자 생성 메서드를 활용한 비교자 (92쪽)
private static final Comparator<PhoneNumber> COMPARATOR =
        comparingInt((PhoneNumber pn) -> pn.areaCode)
                .thenComparingInt(pn -> pn.prefix)
                .thenComparingInt(pn -> pn.lineNum);

위 코드에서 함께 언급된 내용으로 comparingInt에서는 타입을 명시적으로 선언해야 했지만, 그 다음부터인thenComparingInt에서는 타입추론이 잘 작동한다.

  • 타입 추론은 var로 변수선언을 할 때도 쓰인다.
  • compareTo에서 수를 비교할 경우 단순히 두 수를 더하거나 뺀 값으로 반환한다면 정수 오버플로(또는 언더플로)를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있다. 따라서, 각 기본타입의 래퍼 클래스 (이를테면 Integer, String 등)가 제공하는 정적 메서드 compare를 이용하거나 비교자 생성 메서드를 사용하자.
  • 부동소수점 계산의 경우 부동소수점 타입(float, double)의 실제 표현이 지수부와 가수부로 나뉘는데, 가수부가 2진수로 표현되므로 정확한 표현이 어렵다. 특히, 계산을 하게되면 정보의 손실이 발생하기 때문에 정확한 계산이 안될 수 있다.
  • 따라서, 소수점 아래를 내부적으로 Int로 만들어서 계산하는 BigDecimal을 사용하자!