이번에는 Comparable 인터페이스의 유일무이한 메서드인 compareTo를 알아보자
- compareTo는 Object의 메서드가 아니다
- 성격은 두 가지만 빼면 Object의 equals와 같다
- compareTO는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다
- Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서가 있음을 뜻한다
- Comparable을 구현한 객체들의 배열은 다음처럼 손쉽게 정렬할 수 있다.
Arrays.sort(a);
검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 역시 쉽게 할 수 있다
public class WordList{
public static void main(String[] args){
Set<String> s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
}
- 명령줄 인수들을 (중복은 제거하고) 알파벳순으로 출려한다
- String이 Comparable을 구현한 덕분
- Comparable을 구현하여 이 인터페이스를 활용하는 수많은 제네릭 알고리즘과 컬렉션의 힘을 누릴 수 있다
- 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이 Comparable을 구현했다
- 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자
public interface Comparable<T>{
int compareTo(T t);
}
compareTo 메서드의 일반 규약은 equals 규약과 비슷하다
- 이 객체와 주어진 객체의 순서를 비교한다.
- 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다.
- 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.
다음 설명에서 sgn(표현식) 표기는 수학에서 말하는 부호 함수를 뜻하며, 표현식의 값이 음수, 0, 양수 일 때 -1,0,1을 반환한다
* 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을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다
- 이 클래스의 순서는 equals 메서드와 일관되지 않다
- 모든 객체에 대해 전역 동치 관계를 부여하는 equals 메서드와 달리, compareTo는 타입이 다른 객체를 신경 쓰지 않아도 된다
- 타입이 다른 객체가 주어지면 간단히 ClassCAstException을 던져도 되며, 대부분 그렇게 한다
- 이 규약에서는 다른 타입 사이의 비교도 허용하는데, 보통은 비교할 객체들이 구현한 공통 인터페이스를 매개로 이뤄진다
- hashCode 규약을 지키지 못하면 해시를 사용하는 클래스와 어울리지 못하듯,
- compareTo 규약을 지키지 못하면 비교 활용하는 클래스와 어울리지 못한다
- 비교를 활용하는 클래스의 예로는 정렬된 컬렉션인 TreeSet과 TreeMap
- 검색과 정렬 알고리즘을 활용하는 유틸리티 클래스인 Collections와 Arrays가 있다
compareTo 규약을 자세히 살펴보자
- 첫 번째 규약은 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다는 이야기다
- 즉 첫 번째 객체가 두 번째 객체보다 작으면, 두번째 가 첫 번째 보다 커야 한다 (1<2 , 2>1)
- 첫번째 객체가 두 번째와 크기가 같다면, 두 번째는 첫 번째와 같아야 한다 (1=2, 2=1)
- 첫 번째가 두 번째보다 크면, 두 번째는 첫 번째보다 작아야 한다 (1>2, 2 <1)
- 두 번째 규약은 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다
- (1>2, 2>3, 1>3)
- 마지막 규약은 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다
이상의 세 규약은 compareTo 메서드로 수행하는 동치성 검사도 equals 규약과 똑같이
반사성, 대칭성, 추이 성을 충족해야 함을 뜻한다. 그래서 주의 사항도 똑같다
- 기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가했다면 compareTo 규약을 지킬 방법이 없다
- 객체 지향적 추상화의 이점을 포기할 생각이 아니라면 우회 법도 같다
- Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면, 확장하는 대신 독립된 클래스를 만들고,
- 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두자
- 그런 다음 내부 인스턴스를 반환하는 '뷰' 메서드를 제공하면 된다
- 이렇게 하면 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현해 넣을 수 있다
- 클라이언트는 필요에 따라 바깥 클래스의 인스턴스를 필드 안에 담긴 원래 클래스의 인스턴스로 다룰 수도 있다
compareTo의 마지막 규약은 필수는 아니지만 꼭 지키길 권장한다
- compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다는 것이다
- 이를 잘 지키면 compareTo로 줄지은 순서와 equals의 결과가 일관되게 된다.
- compareTo로 줄지은 순서와 equals의 결과가 일관되게 된다.
- compareTo의 순서와 equals의 결과가 일관되지 않은 클래스도 여전히 동작은 한다
- 단 이 클래스의 객체를 정렬된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스에 정의된 동작과 엇박자를 낼 것이다
- 이 인터페이스들은 equals 메서드의 규약을 따른다고 되어 있지만, 놀랍게도 정렬된 컬렉션들은
- 동치성을 비교할 때 equals 대신 compareTo를 사용하기 때문이다
compareTo와 equals가 일관되지 않은 BigDecimal 클래스를 예로 생각해 보자
- 빈 HashSet 인스턴스를 생성한 다음 new BigDecimal("1.0")과 new BigDecimal("1.00")을 차례로 추가한다
- 두 BigDecimal은 equals 메서드로 비교하면 서로 다르기 때문에 HashSet은 원소를 2개가 갖게 된다
- 하지만 HashSet 대신 TreeSet을 사용하면 원소를 하나만 갖게 된다
- compareTo 메서드로 비교하면 두 BigDecimal 인스턴스가 똑같기 때문이다
compareTo 메서드 작성 요령은 equals와 비슷하다 몇 가지 차이점만 주의하면 된다
- Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메서드 인수 타입은 컴파일 타임에 정해진다
- 입력 인수의 타입을 확인하거나 형 변환할 필요가 없다는 뜻이다
- 인수의 타입이 잘못됐다면 컴파일 자체가 되지 않는다
- 또한 null을 인수로 넣어 호출하면 NullPointerException을 던져야 한다
- 물론 실제로도 인수의 멤버에 접근하려는 순간 이 예외가 던져질 것이다
compareTo 메서드는 각 필드가 동치인지를 비교하는 게 아니라 그 순서를 비교한다
- 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출한다
- Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 비 교자를 대신 사용한다
- 비 교자는 직접 만들거나 자바가 제공하는 것 중에 골라 쓰면 된다
코드 14-1 객체 참조 필드가 하나뿐인 비교자
// 코드 14-1 객체 참조 필드가 하나뿐인 비교자 (90쪽)
public final class CaseInsensitiveString
implements Comparable<CaseInsensitiveString> {
// 자바가 제공하는 비교자를 사용해 클래스를 비교한다.
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
}
- CaseInsensitiveString이 Comparable <CaseInsensitiveString>을 구현한 것에 주목하자
- CaseInsensitiveString 참조는 CaseInsensitiveString 참조와만 비교할 수 있다는 뜻
- Comparable을 구현할 때 일반적으로 따르는 패턴이다
이 책 2판에서는 compareTo 메서드에서 정수 기본 타입 필드를 비교할 때는 관계 연산자인 <와>를,
실수 기본 타입 필드를 비교할 때는 정적 메서드인 Double.compare와 Float.compare를 권유
- 자바 7부터는 상황이 변했다
- 박싱 된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare를 이용하면 되는 것이다.
- compareTo 메서드에서 관계 연산자 <와>를 사용하는 이전 방식은 거추장스럽고 오류를 유발하니 추천하지 않는다
코드 14-2 기본 타입 필드가 여럿일 때의 비 교자
// 코드 14-2 기본 타입 필드가 여럿일 때의 비교자 (91쪽)
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 메서드를 구현하는데 멋지게 활용할수 있다
- 많은 프로그래머가 이 방식의 간결함에 매혹되지만, 약간의 성능 저하가 뒤따른다
코드 14-3 비교자 생성 메서드를 활용한 비교자
// 코드 14-3 비교자 생성 메서드를 활용한 비교자 (92쪽)
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>를 반환한다
- 자바의 타입 추론 능력이 이 상황에서 타입을 알아낼 만큼 강력하지 않기 때문에 프로그램이 컴파일되도록 도와준 것
두 전화번호의 지역코드가 같을 수 있으니 비교 방식을 더 다듬어야 한다
- 이 일은 두 번째 비 교자 생성 메서드인 thenCOmparingInt가 수행한다.
- then ComparingInt는 Comparator의 인스턴스 메서드로, int 키 추출자 함수를 입력받아 다시 비 교자를 반환한다
- thenComparingInt는 원하는 만큼 연달아 호출할 수 있다.
- 앞의 예에서는 2개를 연달아 호출했으며, 그중 첫 번째의 키로는 프리픽스를, 두 번째의 키로는 가입자 번호를 사용
- 이번에는 then ComparingInt를 호출할 때 타입을 명시하지 않았다 (자바의 타입 추론 능력이 이 정도는 추론할 수 있다)
Comparator는 수많은 보조 생성 메서드들로 중무장하고 있다
- long과 double용으로는 comparingInt와 thenComparingInt의 변형 메서드를 준비했다
- short처럼 더 작은 정수 타입에는 int용 버전을 사용하면 된다
- 마찬가지로 float은 double용을 이용해 수행한다
객체 참조용 비 교자 생성 메서드도 준비되어 있다
- comparing이라는 정적 메서드 2개가 다중 정의되어있다
- 첫 번째는 키 추출자를 받아서 그 키의 자연적 순서를 사용한다
- 두 번째는 키 추출자 하나와 추출된 키를 비교할 비 교자까지 총 2개의 인수를 받는다
- 또한, thenComparing이란 인스턴스 메서드가 3개 다중 정의되어있다
- 첫 번째는 비 교자 하나만 인수로 받아 그 비 교자로 부차 순서를 정한다
- 두 번째는 키 추출자를 인수로 받아 그 키의 자연적 순서로 보조 순서를 정한다
- 마지막 세 번째는 키 추출자 하나와 추출된 키를 비교할 비 교자까지 총 2개의 인수를 받는다
코드 14-4 해시 코드 값의 차를 기준으로 하는 비 교자 - 추이 성을 위배한다!
static Comparator<Object> hashCodeOrder = new Comparator<>(){
public int compare(Object o1, Object o2){
return o1.hashCode() - o2.hashCode();
}
}
- 이 방식은 사용하면 안 된다 , 정수 오버플로를 일으키거나 IEEE 754 부동소수점 계산 방식에 따른 오류를 낸다
- 그렇다고 이번 아이템에서 설명한 방법대로 구현한 코드보다 월등히 빠르지도 않을 것이다
그 대신 다음의 두 방식 중 하나를 사용하자
코드 14-5 정적 compare 메서드를 활용한 비 교자
static Comparator<Object> hashCodeOrder = new Comparator<>(){
public int compare(Object o1, Object o2){
return Integer.compare(o1.hashCode(), o2.hashCode());
}
}
코드 14-6 비 교자 생성 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder =
Comparator.comparingInt(o -> o.hashCode());
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여,
그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다
compareTo 메서드에서 필드의 값을 비교할 때 <와> 연산자는 쓰지 말아야 한다.
그 대신 박싱 된 기본 타입 클래스가 제공하는 정적 compare 메서드나
Comparator 인터페이스가 제공하는 비 교자 생성 메서드를 사용하다
'Books > Effective-Java 3판' 카테고리의 다른 글
15. 클래스와 맴버의 접근 권한을 최소화하라 (0) | 2021.10.11 |
---|---|
13. clone 재정의는 주의해서 진행하라(2) (0) | 2021.10.06 |
13. clone 재정의는 주의해서 진행하라(1) (0) | 2021.10.05 |
12. toString을 항상 재정의하라 (0) | 2021.10.03 |
11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2021.10.02 |