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

2024. 9. 25. 23:23·1️⃣ 백앤드/이펙티브 자바

1. Comparable 인터페이스의 유일한 메서드 compareTo

compareTo 는 단순 동치성 비교 + 순서 비교가 가능하다. compareTo 를 구현했다는 것은, 순서가 존재하다는 것이고, Arrays.sort 를 활용한 정렬이 가능하다는 것이다. String 이 CompareTo 를 구현한 덕분에, 자동 정렬되는 TreeSet 자료구조 상, 출력하면 알파벳순으로 정렬되어 출력된다.

public class Main {
    public static void main(String[] args) {
        Set<String> s = new TreeSet<>();
        Collections.addAll(s, args);
        System.out.println(s);
    }
}

 

2. compareTo 일반 규약

객체 A와 객체 B의 순서를 비교한다. A객체가 B객체보다 작으면 음수, 같으면 0, 크면 양수를 반환허고, 비교할 수 없는 객체 타입이면 ClassCastException 을 던진다.

 

아래 설명에서 sgn은 부호함수를 뜻하며, 표현식의 값이 음수, 0, 양수일 때 -1, 0, 1을 반환하도록 정의했다.

Comparable을 구현한 클래스는 대칭성을 보장해야 한다.
Comparable을 구현한 클래스는 모든 x, y에 대하여 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))여야 한다.

 

두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야한다. 첫번째 객체 > 두번째 객체면, 두번째 객체 < 첫번째 객체 여야 한다.

Comparable을 구현한 클래스는 추이성을 보장해야 한다.
즉, (x.compareTo(y) > 0 && y.compareTo(z) > 0)이면 x.compareTo(z) > 0 이다.

 

첫번째 객체가 두번째 객체보다 크고, 두번째 객체가 세번째 객체보다 크면, 첫번째 객체는 세번째 객체보다 커야한다.

Comparable을 구현한 클래스는 반사성을 보장해야 한다.
Comparable을 구현한 클래스는 모든 z에 대해

x.compareTo(y) == 0이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))이다.

 

크기가 같은 객체들 간에는 어떤 객체와 비교하더라도 같아야 한다.

(필수는 아니지만 지키는게 좋음)
(x.compareTo(y) == 0) == (x.equals(y))여야 한다.

 

compareTo 메서드로 수행한 동치성 결과가, equals 와 같아야 한다. 이를 잘 지키면, compareTo 에 의해 줄지어진 순서와 equals 의 결과가 동일하게 되어 정렬된 컬렉션에서도 확실하게 사용할 수가 있게된다. compareTo 와 equals 가 일관되지 않는 BigDecimal 클래스를 예시로 확인해보자.

final BigDecimal bigDecimal1 = new BigDecimal("1.0");
final BigDecimal bigDecimal2 = new BigDecimal("1.00");

final HashSet<BigDecimal> hashSet = new HashSet<>();
hashSet.add(bigDecimal1);
hashSet.add(bigDecimal2);

System.out.println(hashSet.size());

final TreeSet<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(bigDecimal1);
treeSet.add(bigDecimal2);

System.out.println(treeSet.size());

// 실행결과 
hashSet: 2
treeSet: 1

 

compareTo 메서드로 비교하면 두 BigDecimal 인스턴스가 동일하기 때문에, 원소를 하나만 갖게 된다. 

compareTo 규약을 지키지 않는다면, 비교를 활용하는 TreeSet, TreeMap, Collectionsm Arrays 등과 어울릴 수 없다.

 

3. compareTo 작성 요령

클래스에 핵심 필드가 여러개라면, 가장 핵심적인 필드부터 비교하자. 가장 핵심적인 필드가 같다면, 똑같지 않은 필드를 찾아나갈 때 까지 그 다음으로 중요한 필드를 비교하고, 비교 결과가 0이 아니라면 곧장 반환하자.

public int compare(final PhoneNumber phoneNumber) {
        int result = Short.compare(areaCode, phoneNumber.areaCode); // 가장 중요

        if (result == 0) {
            result = Short.compare(prefix, phoneNumber.prefix); // 그 다음 중요
            if (result == 0) {
                result = Short.compare(lineNum, phoneNumber.lineNum); // 다다음 중요
            }
        }

        return result;
}

 

4. Comparator 

자바 8 이후, Comparator 인터페이스 (Comparable 유사품) 가 일련의 비교자 생성 메서드와 함께 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다. 이를 통해 compareTo 메서드를 구현할 수 있는데, 예시를 통해 살펴보자.

Comparator 인터페이스는 많은 비교자 생성 메서드를 제공한다.

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

public int compareTo(PhoneNumber phoneNumber) {
	    return COMPARATOR.compare(this, phoneNumber);
}

 

성능은 살짝 뒤떨어지지만, 코드가 훨씬 깔끔해진다. 비교자 생성 메서드인 comparingInt 를 통해 지역 코드를 비교하고, 같을 경우를 대비해 thenComparingInt 를 수행해 프리픽스, 가입자 번호를 연달아 호출하며 비교한다.

또한, comparator 인터페이스는 수많은 보조 생성 매서드들이 있다. long 과 double 을 대비해 comparingInt 와 thenComparingInt 의 변형 메서드들이 있다. 

 

Comparator 인터페이스를 활용한 정적 compare 메서드, 비교자 생성 메서드를 활용한 '값의 차' 비교자 예시이다.

static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
        @Override
        public int compare(final Object o1, final Object o2) {
            return Integer.compare(o1.hashCode(), o2.hashCode());
        }
};
static Comparator<Object> hashCodeOrder = 
Comparator.comparingInt(o -> o.hashCode());

 

5. 결론

순서를 고려해야 하는 값 클래스를 작성한다면, 꼭 Comparable 인터페이스를 구현하여, 정렬/검색/비교 기능을 제공하는 컬렉션과 어우러지도록 하자.
compareTo 메서드에서 필드 값 비교 시, '<' '>' 연산자 대신 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.

'1️⃣ 백앤드 > 이펙티브 자바' 카테고리의 다른 글

[아이템16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라  (0) 2024.10.05
[아이템15] 클래스와 멤버의 접근 권한을 최소화하라  (0) 2024.10.04
[아이템13] clone 재정의는 주의해서 진행하라  (0) 2024.09.25
[아이템12] toString 을 항상 재정의하라  (1) 2024.09.25
[아이템11] equals 를 재정의하려거든 hashCode 도 재정의하라  (3) 2024.09.24
'1️⃣ 백앤드/이펙티브 자바' 카테고리의 다른 글
  • [아이템16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
  • [아이템15] 클래스와 멤버의 접근 권한을 최소화하라
  • [아이템13] clone 재정의는 주의해서 진행하라
  • [아이템12] toString 을 항상 재정의하라
HOZINU
HOZINU
주니어 백앤드 개발자의 세상만사 이모저모. 주로 개발 이야기를 다룸.
  • HOZINU
    백엔드 탐험 일지
    HOZINU
  • 전체
    오늘
    어제
  • 블로그 메뉴

    • ⛪ HOME
    • 🌍 GITHUB
    • 카테고리 (73)
      • 1️⃣ 백앤드 (72)
        • 이펙티브 자바 (72)
      • 2️⃣ CS (0)
        • 운영체제 (0)
        • 네트워크 기초 (0)
        • 네트워크 응용 (0)
        • SSL & PKI (0)
        • 기타 (0)
      • 3️⃣ 코딩테스트 (0)
      • 4️⃣ 개인공부 (0)
        • MSA (0)
        • REDIS (0)
      • 5️⃣ 일상이야기 (1)
  • 인기 글

  • 태그

    정적 팩터리 메서드
    hashcode
    캡슐화
    멤버클래스
    Comparable
    Cleaner
    로타입
    계층구조
    CLONE
    맥북
    정보은닉
    try-with-resources
    컴포지션
    equals
    빌더
    싱글턴
    표준예외
    optional
    finalizer
    의존객체
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.2
HOZINU
[아이템14] Comparable 을 구현할지 고려하라
상단으로

티스토리툴바