알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하라.
public interface Comparable<T> {
int compareTo(T t);
}
compareTo는 Comparable 인터페이스의 유일무이한 메서드인데 equals와 유사하지만 Object의 메서드가 아니다.
단순 동치성 비교에 더해 순서까지 비교할 수 있으며 제네릭하다.
compareTo 규약을 지키지 못하면 비교를 활용하는 클래스와 어울리지 못한다. : TreeSet TreeMap Collections Arrays
Comparable 구현의 이점
- Comparable을 구현한 클래스의 인스턴스들은 자연적인 순서가 있어 다음과 같이 간단히 정렬이 가능하다.
Arrays.sort(a);
- 제네릭 알고리즘과 Collections의 힘을 누릴 수 있다.
equals와의 차이
- 모든 객체에 대해 전역 동치관계를 부여하는 equals와 달리 compareTo는 타입이 다른 객체를 신경쓰지 않아도 된다.
- 타입이 다른 객체 끼리의 비교도 허용하지만 대부분 간단히 ClassCaseException을 던진다.
- Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메서드의 인수 타입은 컴파일 타임에 정해지기 때문에 입력 인수의 타입을 확인하거나 형변환할 필요가 없다.
- Null을 인수로 넣어 호출하면 NullPointerException을 던져야 한다.
compareTo 메서드의 일반 규약
이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.
- Comparable을 구현한 클래스는 모든 x, y에 대해 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))여야 한다(따라서 x.compareTo(y)는 y.compareTo(x)가 예외를 던질 때에 한해 예외를 던져야 한다).
- ⇒ 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다. 반사성
- Comparable을 구현한 클래스는 추이성을 보장해야 한다. 즉, (x.compareTo(y) > 0 && y.compareTo(z) > 0)이면x.compareTo(z) > 0이다.
- ⇒ 첫 번째가 두 번째보다 크고 두 번재가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다. 대칭성
- Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))다.
- ⇒ 크기가 같은 객체끼리는 어떤 객체와 비교하더라도 항상 같아야 한다. 추이성
- 이번 권고가 필수는 아니지만 꼭 지키는 게 좋다. (x.compareTo(y) == 0) == (x.equals(y))여야 한다. Comparable을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다. 다음과 같이 명시하면 적당할 것이다.
- compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다.
- 지키지 않은 경우 이 컬렉션이 구현한 인터페이스(Collection, Set, Map)에 정의된 동작과 엇박자를 낼 것이다.
/** BigDecimal.java : equals와 compareTo가 일관되지 않는 예 */ BigDecimal bd1 = new BigDecimal("1.0"); BigDecimal bd2 = new BigDecimal("1.00"); bd1.equals(bd2); // false HashSet<BigDecimal> hs = new Set<BigDecimal>(); hs.add(bd1); hs.add(bd2); // bd1의 bd2의 hashCode가 서로 다름 => hs에 원소 2개 bd1.compareTo(b2); // 0 TreeSet<BigDecimal> ts = new Set<BigDecimal>(); ts.add(bd1); ts.add(bd2); // compareTo로 비교하면 두 인스턴스가 동일하기 때문에 => ts에 원소 1개
- *이 인터페이스들은 equals 메서드의 규약을 따른다고 되어 있지만 실제로는 동치성을 비교할 때 compareTo를 사용한다.
*sgn(표현식) 표기는 수학에서 말하는 부호 함수(signum function)를 뜻 하며, 표현식의 값이 음수, 0, 양수일 때 -1, 0, 1을 반환하도록 정의했다.
작성 요령
- 각 필드가 동치인지를 비교하는 게 아니라 그 순서를 비교해야 한다.
- 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출한다.
- Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해 야 한다면 비교자(Comparator)를 대신 사용한다.
- 비교자는 직접 만들거나 자바가 제공하는 것 중에 골라 쓰면 된다.
- public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> { public int compareTo(CaseInsensitiveString cis) { return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s); } ... }
- compareTo 메서드에서 관계 연산자 <, >를 사용하는 이전 방식은 거추장스럽고 오류를 유발하니 박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare를 이용해라.
- 핵심 필드가 여러개라면 비교 순서가 중요하다. 가장 핵심적인 필드부터 비교 해 비교 결과가 0이 아니라면 바로 반환한다.
- public int compareTo(PhoneNumber pn) { int result = Short.compare(areaCode, pn.areaCode); // 가장 중요한 필드 if (result == 0) { result = Short.compare(lineNum, pn.lineNum); // 두 번째로 중요한 필드 if (result == 0) { result = Short.compare(prefix, pn.prefix); // 세 번째로 중요한 필드 } } return result; }
- 자바 8 부터는 Comparable을 사용해 구현할 수 있다.
주의점
값의 차를 기준으로 비교하지 않아야 한다.
: 정수 오버플로를 일으키기나 IEEE 754 부동소수점 계산 방식에 따른 오류를 낼 수 있다.
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
▼ 권장하는 방법
# 정적 compare 메서드를 활용한 비교자
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());
Comparable
Comparator 인터페이스가 메서드 연쇄 방식으로 비교자를 생성할 수 있어 이 비교자들을 이용해 구현할 수 있으나 약간의 성능 저하가 있다.
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
// 람다를 인수로 받으며 이 람다는 PhoneNumber에서 추출한 지역 코드를 기준으로 전화번호의 순서를 정하는 Comparator<PhoneNumber>를 반환
// 입력 인수에 타입 PhoneNumber을 명시해 자바의 타입 추론 능력으로 타입을 알아낼 수 없는 부분을 명시함
.thenComparingInt(pn -> pn.prefix) // 두 전화번호의 지역 코드가 같을 수 있으니 추가 비교함 - prefix
// 자바의 타입 추론 능력으로 추론할 수 있어 타입을 명시하지 않았음
.thenComparingInt(pn -> pn.lineNum); // 추가비교 - 가입자 번호
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
- 자바의 숫자용 기본 타입을 모두 커버하는 보조 생성 메서드들이 있다. comparingInt 등
- comparing 객체 참조용 비교자 생성 메서드
- 키 추출자를 받아서 그 키의 자연적 순서를 사용한다.
- 키 추출자 하나와 추출된 키를 비교할 비교자까지 총 2개의 인수를 받는다.
- thenComparing 인스턴스 메서드
- 비교자 하나만 인수로 받아 그 비교자로 부차 순서를 정한다.
- 키 추출자를 인수로 받아 그 키의 자연적 순서로 보조 순서를 정한다.
- 키 추출자 하나와 추출된 키를 비교할 비교자까지 총 2개의 인수를 받는다.
Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶은 경우
(equals와 동일함 : 아이템 10)
기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가 했다면 compareTo 규약을 지킬 방법이 없어 다음과 같은 방법으로 우회할 수 있다.
- 확장하는 대신 독립된 클래스를 만든다.
- 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두고 내부 인스턴스를 반환하는 뷰 메서드를 제공한다.
- 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현한다.
순서를 고려하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하자. compareTo 메서드에서 필드의 값을 비교할 때는 <, > 연산자를 쓰지 않고 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
'reviews > Effective JAVA' 카테고리의 다른 글
015. 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2022.05.26 |
---|---|
013. clone 재정의는 주의해서 진행하라 (0) | 2022.05.05 |
012. toString을 항상 재정의하라 (0) | 2022.05.02 |
011. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2022.04.21 |
010. equals는 일반 규약을 지켜 재정의하라 (0) | 2022.04.07 |