[이펙티브 자바] 아이템 14. Comparable을 구현할지 고려하라

June·2022년 2월 24일
0

[이펙티브자바]

목록 보기
13/72

Comparable을 구현햇다는 것은 그 클래스의 인스턴스들에는 자연적인 순서가 있다는 것을 뜻한다. 그래서 Comparable을 구현한 객체들의 배열은 손쉽게 정렬이 가능하다.

Arrays.sort(a);

자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이 Comparable을 구현했다.
알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.

compareTo 메서드의 규약은 equals의 규약과 비슷하다.
이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.

규약

1.

  • 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다.

2.

  • a > b, b > c -> a > c

3.

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

이는 반사성, 대칭성, 추이성을 충족해야 함을 뜻한다. (자세한거는 아이템10에서 equals 규약을 참조하자.)

그래서 기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가했다면 compareTo 규약을 지킬 방법이 없다.

그래서 우회법도 마찬가지로 Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면, 확장하는 대신 독립된 클래스를 만들고, 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 둔다. 그러고 내부 인스턴스를 반환하는 '뷰' 메서드를 제공하면 된다. 이렇게 하면 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현할 수 있다.

참고
Comparable vs compareTo

보너스 4.

  • compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다.
    이를 잘 지키면 compareTo로 줄지은 순서와 equals의 결과가 일관된다.
    comapreTo와 equals의 결과과 일관되지 않아도 작동은 하지만, 정렬된 컬레션(Collection, Set, Map)에 넣으면 정의된 동작과 엇박자가 난다. 이 인터페이스들은 equals 규약을 따른다고 되어있지만 동치성을 비교할때 comapreTo를 사용한다.

ex> HashSet 인스턴스에 new BigDecimal("1.0")new BigDecimal("1.00")을 추가하면 두개로 인식된다. 그러나 TreeSet에서는 원소를 하나만 갖게된다.

일반적인 Comparable

public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
    ...
}

CaseInsensitiveStringComparable<CaseInsensitiveString>을 구현헀다. CaseInsensitiveString끼리만 비교할 수 있다는 뜻으로, Comparable을 구현할 때 일반적인 패턴이다.

compareTo 구현 경험

    public int compareTo(Car o) {
        if (this.position.isSmallerThan(o.position)) {
            return -1;
        } else if (this.position.equals(o.position)) {
            return 0;
        }
        return 1;
    }

Comparable 잘 구현해주셨어요.

여기서 조금 더 생각을 진전시켜볼까요?
Car가 비교 가능하다면 Position은 좀 더 당연하게 비교 가능한 객체가 아닐까 싶어요.
그래서 만약 Position 또한 Comparable의 구현체라고 한다면 그 내부는 Integer.compare() 이라는 안정된 API를 사용할 수 있을 거 같네요.
직접 compareTo를 구현하고 -1, 0, 1을 반환하는 건 가독성이나 실수의 가능성을 생각해봤을 때 피할 수 있으면 피하면 좋다고 생각합니다 :)

자바 7이전까지는 compareTo 메서드에서 정수 기본 타입 필드를 비교할 때는 관계 연산자인 <>를, 실수 기본 타입을 비교할때는 Double.compareFloat.compare를 사용하라고 권햇다.

자바7부터는 박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare를 이용하면된다.

public int compareTo(Car o) {
    return Integer.compare(this.position, o.getPosition());
}

Quiz

public class Position implements Comparable<Position> {
    private final int position;
    
    ...
    @Override
    public int compareTo(Position target) {
        return position - target.position;
    }
}

이렇게 하면 또 다른 문제점은 뭘까?
오버플로우나 언더플로우가 날 수도 있다는 점이다.

기본 타입 필드가 여럿일 때 비교자

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

가장 핵심적인 필드부터 비교해나가서 결정되면 바로 반환하면 된다.

정적 compare 메서드를 이용한 비교자

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

정렬 기준인 필드가 여러 개일 때

Comparator 인터페이스의 비교자 생성 메서드를 활용

private static final Comprator<Book> COMPARATOR = Comparator.comparingInt((Book book) -> book.getprice)
													.thenComparaing(book -> book.title);

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

https://github.com/woowacourse-study/2022-effective-java/tree/main/03%EC%9E%A5/%EC%95%84%EC%9D%B4%ED%85%9C_14

0개의 댓글