Comparable 인터페이스의 유일무이한 메서드인 compareTo를 알아보자. compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다. compareTo 메서드의 일반 규약은 equals의 규약과 비슷하다.
이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.
다음 설명에서 sgn(표현식) 표기는 수학에서 말하는 부호 함수를 뜻하며 표현식의 값이 음수, 0, 양수일 때, -1, 0, 1을 반환하도록 정의했다.
1. Comparable을 구현한 모든 클래스는 모든 x, y에 대해 sgn(x.compareTo(y)) == -sgn(y.compareTo.(x))여야 한다.
- 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다
2. Comparable을 구현한 클래스는 추이성을 보장해야 한다. 즉, (x.compareTo(y) > 0 && y.compareTo(z) > 0)이면 x.compareTo(z) > 0이다.
- 첫 번째가 두 번째보다 크고, 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다는 뜻이다.
3. Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))다.
- 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다는 뜻이다.
이번 권고가 필수는 아니지만, (x.compareTo(y) == 0) == (x.compareTo(y)) 여야 한다. Comparable을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다. 다음과 같이 명시하면 적당할 것이다.
※ "주의: 이 클래스의 순서는 equals 메서드와 일관되지 않다."
이상의 규약들은 compareTo 메서도르 수행하는 동치성 검사도 equals 규약과 똑같이 반사성, 대칭성, 추이성을 충족해야 함을 뜻한다.
Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면, 확장하는 대신 독립된 클래스를 만들고, 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두자. 그런 다음 내부 인스턴스를 반환하는 '뷰' 메서드를 제공하면 된다. 이렇게 하면 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현해넣을 수 있다.
compareTo 메서드는 각 필드가 동치인지를 비교하는게 아니라 그 순서를 비교한다. 다음 코드는 CaseInsensitiveString용 compareTo 메서드로, 자바가 제공하는 비교자를 사용하고 있다. Compareble란 CaseInsensitiveString의 참조는 CaseInsensitiveString 참조와만 비교할 수 있다는 뜻이다.
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
// ...
}
compareTo 메서드에서 관계 연산자 >, <를 사용하는 이전 방식은 추천하지 않는다. 다음은 PhoneNumber 클래스용 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;
}
자바 8에서는 Comparator 인터페이스가 일련의 비교자 생성 메서드와 팀을 꾸려 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다. 이 비교자들을 Comparable 인터페이스가 원하는 compareTo 메서드를 구현하는데 멋지게 활용할 수 있다. 하지만, 약간의 성능 저하가 뒤따른다.
대신 자바의 정적 임포트 기능을 이용하면 코드가 훨씬 깔끔해진다.
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.araeCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
이외에도 아래와 같이 '값의 차'를 기준으로 첫 번째 값이 두 번째 값보다 작으면 음수를, 두 값이 같으면 0을, 첫 번째 값이 크면 양수를 반환하는 compareTo나 compare 메서드와 마주할 것이다.
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
이러한 방식은 사용하면 안된다. 정수 오버플로를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있다. 대신에 아래 두 가지 방식을 고려하자.
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());
'Developer's_til > Effective Java' 카테고리의 다른 글
[item 17] 변경 가능성을 최소화하라 (0) | 2022.11.01 |
---|---|
[item 15] 클래스와 멤버의 접근 권한을 최소화하라. (0) | 2022.10.28 |
[item 13] 재정의는 주의해서 진행하라 (0) | 2022.10.24 |
[item 11] equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2022.10.19 |
[item 10] equals는 일반 규약을 지켜 재정의하라 (0) | 2022.10.13 |