아이템 14. Comparable을 구현할지 고려하라
Comparable
인터페이스의 compareTo
메서드는 두 가지만 빼면 Object
의 equals
와 같다. 다른 두 가지는 다음과 같다.
compareTo
는 단순 동치성 비교에 더해 순서까지 비교할 수 있다.- 제너릭하다.
Comparable
을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서가 있다는 것이다. 그래서 Comparable
을 구현한 객체들의 배열은 다음처럼 쉽게 정렬할 수 있다.
Arrays.sort(a);
검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게 할 수 있다. String
또한 Comparable
이 구현되어 있어서 TreeSet
을 사용하면 중복을 제거하고 알파벳순으로 쉽게 정렬할 수 있다. Comparable
을 구현하면 이 인터페이스를 활용하는 수많은 제너릭 알고리즘과 컬렉션의 힘을 누릴 수 있다. 조그만 노력으로 엄청난 효과를 얻을 수 있는 것이다. 사실상 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이 Comparable
을 구현했다. 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable
인터페이스를 구현하자.
다음은 compareTo
메서드의 일반 규약이다. equals
규약과 비슷하다
이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.
다음 설명에서 sgn(표현식) 표기는 수학에서 발하는 부호함수를 뜻하며, 표현식의 값이 음수, 0, 양수일 때는 -1, 0, 1을 반환하도록 정의했다.
- Comparable을 구현한 클래스는 모든 x, y에 대해 sgn(x.compareTo(y))==-sgn(y.compareTo(x))dudi gksek.
- 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을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다. 다음과 같이 명시하면 적당하다.
"주의: 이 클래스의 순서는 equals 메서드와 일관되지 않다."
모든 객체에 대해 전역 동치관계를 부여하는 equals
메서드와 달리, compareTo
는 타입이 다른 객체를 신경 쓰지 않아도 된다. 타입이 다른 객체가 주어지면 간단히 ClassCastException
을 던져도 되며, 대부분 그렇게 한다. 물론, 이 규약에서는 다른 타입 사이의 비교도 허용하는데, 보통은 비교할 객체들이 구현한 공통 인터페이스를 매개로 이뤄진다.
규약에 대해 자세히 알아보자.
첫 번째 규약은 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다는 얘기다. 객체 A
, B
로 설명하겠다. A<B
이면 B>A
여야 하고 A==B
이면 B==A
여야하고 A>B
이면 B<A
여야 한다는 것이다.
두 번째 규약은 추이성 이야기이다. 객체 A
, B
, C
로 설명하겠다. A>B
이고 B>C
이면 A>C
여야 한다는 것이다.
마지막 규약은 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다는 뜻이다.
위의 세 규약은 compareTo
메서드로 수행하는 동치성 검사도 equals
규약과 똑같이 반사성, 대칭성, 추이성을 만족해야 함을 뜻한다. 그래서 주의사항도 똑같다. 기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가했다면 compareTo
규약을 지킬 방법이 없다. 우회법도 같다.
compareTo
의 마지막 규약은 필수는 아니지만 꼭 지키킬 권한다. 마지막 규약은 간단히 말하면 compareTo
메서드로 수행한 동치성 테스트의 결과가 equals
와 같아야 한다는 것이다. 이를 잘 지키면 compareTo
로 줄지은 순서와 equals
의 결과가 일관되게 한다. compareTo
의 순서와 equals
의 결과가 일관되지 않아도 클래스는 여전히 동작은 한다. 단, 이 클래스의 객체를 정렬된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스(Collection, Set 혹은 Map)에 정의된 동작과 엇박자를 낸다. 이 인터페이스들은 equals
규약을 따른다고 되어있지만, 정렬된 컬렉션들은 동치성을 비교할 때 equals
대신 compareTo
를 사용하기 때문이다. 아주 큰 문제는 아니지만 주의해야 한다.
compareTo
메서드 작성 요령은 equals
와 비슷하다. 몇 가지 차이점만 주의하면 되는데 다음과 같다.
Comparable
은 타입을 인수로 받는 제네릭 인터페이스이므로compareTo
메서드의 인수 타입은 컴파일 타입에 정해진다. 입력 인수의 타입을 확인하거나 형변환할 필요가 없다. 인수의 타입이 잘못됐다면 컴파일 자체가 되지 않는다. 또한null
을 인수로 넣어 호출하면NullPointerException
을 던져야 한다. 물론 실제로도 인수의 멤버에 접근하려는 순간 이 예외가 던져진다.
compareTo
메서드는 각 필드가 동치인지를 비교하는 게 아니라 그 순서를 비교한다. 객체 참조 필드를 비교하려면 compareTo
메서드를 재귀적으로 호출한다. Comparable
을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 비교자를 대신 사용한다. 비교자는 직접 만들거나 자바가 제공하는 것 중에 골라 쓰면 된다.
public final class CaseInsensitiveString
implements Comparable<CaseInsensitiveString> {
private final String s;
// 자바가 제공하는 비교자를 사용해 클래스를 비교한다.
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
...
}
박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare
이용하는 것을 추천한다. 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
인터페이스가 일련의 비교자 생성 메서드와 팀을 꾸려 메서드 연쇄 방식으로 비교자를 생성할 수 있게 됐다. 코드가 깔끔해지긴 하지만 조금 느려질 수 있다는 것을 알아야 한다.
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
위의 코드는 클래스를 초기화할 때 비교자 생성 메서드 2개를 이용해 비교자를 생성한다. 그 첫 번째인 comparingInt
는 객체 참조를 int
타입 키에 매핑하는 키 추출 함수를 인수로 받아, 그 키를 기준으로 순서롤 정하는 비교자를 반환하는 정적 메서드다. 앞의 예에서 comparingInt
는 람다를 인수로 받으며, 이 람다는 PhoneNumber
에서 추출한 지역 코드를 기준으로 전화번호의 순서를 정하는 Comparator<PhoneNumber>
를 반환한다. 자바의 타입 추론 능력이 강력하지 않기 때문에 (PhoneNumber pn)
과 같이 인수의 타입을 명시해서 프로그램이 컴파일되도록 도와줬다.
두 전화번호의 지역 코드가 같을 수 있는 상황도 고려해야 한다. 이 일은 두 번째 비교자 생성 메서드인 thenComparingInt
가 수행한다. thenComparingInt
는 Comparator
의 인스턴스 메서드로, int
키 추출자 함수를 입력 받아 다시 비교자를 반환한다.(이 비교자는 첫 번째 비교자를 적용한 다음 새로 추출한 키로 추가 비교를 수행한다.) thenComparingInt
는 원하는 만큼 연달아 호출할 수 있다. thenComparingInt
를 호출할 때는 타입을 명시하지 않았는데 이 정도는 또 자바가 추론할 수 있다.
Comparator
는 수많은 보조 생성 메서드들을 가지고 있다. long
과 double
용으로는 comparingInt
와 thenComparingInt
의 변형 메서드를 준비했다. short
처럼 더 작은 정수 타입에는 int
용 버전을 사용하면 된다. 마찬가지로 float
은 double
용을 이용해 수행한다. 이런 식으로 자바의 숫자용 기본 타입을 모두 커버한다.
객체 참조용 비교자 생성 메서드도 준비되어 있다. 우선, comparing
이라는 정적 메서드 2개가 다중 정의되어 있다. 첫 번째는 키 추출자를 받아서 그 키의 자연적 순서를 사용한다. 두 번째는 키 추출자와 하나와 추출된 키를 비교할 비교자까지 총 2개의 인수를 받는다. 또한, thenComparing
이란 인스턴스 메서드가 3개 다중 정의되어 있다. 첫 번째는 비교자 하나만 인수로 받아 그 비교자로부터 순서를 정한다. 두 번째는 키 추출자를 인수로 받아 그 키의 자연적 순서로 보조 순서를 정한다. 마지막 세 번째는 키 추출자 하나와 추출된 키를 비교할 비교자까지 총 2개의 인수를 받는다.
이따금 '값의 차'를 기준으로 첫 번째 값이 두 번째 값보다 작으면 음수를, 두 값이 같으면 0을, 첫 번째 값이 크면 양수를 반환하는 compareTo
나 compare
메서드를 볼 수 있다.
static Comparator<Object> hashCodeOrder = new Comparator<>(){
public int compare(Object o1, Object o2){
return o1.hashCode() - o2.hashCode();
}
}
위와 같은 방식을 쓰면 안된다. 이 방식은 정수 오버플로우를 일으키거나 IEEE 754
부동소수점 계산 방식에 따른 오류를 낼 수 있다. 그렇다고 이번 아이템에서 설명한 방법대로 구현한 코드보다 월등히 빠르지도 않다. 그 대신 다음 두 방식 중 하나를 사용하면 된다.
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());
Author And Source
이 문제에 관하여(아이템 14. Comparable을 구현할지 고려하라), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@mbsik6082/아이템-14.-Comparable을-구현할지-고려하라저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)