# ITEM14 Comparable 을 구현할지 고려하라
순서를 고려해야 하는 값 클래스를 작성 → 꼭 Comparable 인터페이스를 구현
- 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야한다.
# compareTo
Comparable 인터페이스의 유일한 메서드
- Object 메서드가 아님
- Object equals 와 유사함
# compareTo 특징
- compareTo 는 단순 동치성 비교 + 순서 비교
- Comparable 을 구현한 클래스
- 객체는 자연 순서 존재
- 객체들의 배열은
Arrays.sort(a);
로 정렬 가능
- Comparable 을 구현하면 이 인터페이스를 활용하는 수 많은 제네릭 알고리즘과 컬렉션의 효과를 사용할 수 있음
- 자바 플랫폼 라이브러리의 모든 값 클래스와 열거타입[34]이 Comparable 을 구현함.
- 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자
- 검색, 극단값 계산, 자동 정렬되는 컬렉션 관리 가능
- 아래 코드는 String 이 Comparable 을 구현 → 명령줄 인수들을 알파벳 순으로 출력함
public class WordList { public static void main(String[] args) { Set<String> s = new TreeSet<>(); Collections.addAll(s, args); System.out.println(s); } }
1
2
3
4
5
6
7
# compareTo 일반 규약
equals 규약과 유사함
public interface Comparable<T> {
int compareTo(T t);
}
1
2
3
2
3
이 객체와 주어진 객체의 순서를 비교한다.
- 이 객체가 주어진 객체보다
- 작으면 음의정수 반환
- 같으면 0 반환
- 크면 양의정수 반환
- 이 객체와 비교할 수 없는 타입의 객체가 주어지면
- ClassCastException 을 던짐
sng(표현식) 표기
부호 함수(signum function)
표현식의 값이 음수, 0, 양수 일 때 -1, 0, 1 을 반환하도록 정의됨
Comparable 을 구현한 클래스는
- 모든 x, y 에 대해
sng(x.compareTo(y)) == - sgn(y.compareTo(x))
이다- (따라서
x.compareTo(y)
는y.compareTo(x)
가 예외를 던질 때에 한해 예외를 던져야 한다.)
- 추이성을 보장해야 한다.
(x.compareTo(y) > 0 && y.compareTo(z) > 0)
이면x.compareTo(z) > 0
이다
- 모든 z 에 대해
x.compareTo(y) == 0
이면sgn(x.compareTo(z)) == sgn(y.compareTo(z))
- 필수는 아니지만 꼭 지키는게 좋은 항목
(x.compareTo(y) == 0) == (x.equals(y))
여야 한다.- compareTo 로 수행한 동치성 테스트의 결과가 equals 와 같아야 한다.
- 권고를 지키지 않는 클래스
- "주의: 이 클래스의 순서는 equals 메서드와 일관되지 않다." 명시해야 함
# equals 와 compareTo
# equals
- 모든 객체에 대해 전역 동치관계를 부여함
- 타입 캐스팅 필요
# compareTo
타입이 다른 객체를 신경쓰지 않아도 됨
타입이 다른 객체가 주어지면 ClassCastException 을 던지면 됨.
규약에서는 다른 타입 비교도 허용하지만,
보통 비교할 객체들이 구현한 공통 인터페이스(Collections, Set, Map)를 매개로 이뤄진다.hashCode 규약을 지키지 못하면 해시를 사용하는 클래스와 어울리지 못하듯,
compareTo 규약을 지키지 못하면 비교를 활용하는 클래스와 어울리지 못한다.- 비교를 활용하는 클래스
- 정렬된 컬렉션 TreeSet, TreeMap
- 검색과 정렬알고리즘을 활용하는 Collections, Arrays
- 해당 컬렉션이 구현한 인터페이스(Collections, Set, Map..) 에 정의된 동작과 어울리지 못함.
- 이 인터페이스들은 equals 메서드의 규약을 따른다고 되어 있지만, 동치성을 비교할 때 compareTo 를 사용함
- 비교를 활용하는 클래스
compareTo 와 equals 가 일관되지 않는 BigDecimal 클래스
- HashSet 인스턴스를 생성한 다음
new BigDecimal("1.0")
,new BigDecimal("1.00")
을 추가한다.- equals 메서드로 비교하면 서로 다르기 때문에 hashSet 은 원소를 2개 갖는다.
- TreeSet 인스턴스를 사용하면 원소 1개 만 갖는다.
- compareTo 메서드로 비교하면 두 bigDecimal 인스턴스가 같다.
- HashSet 인스턴스를 생성한 다음
# Comparable 을 구현한 클래스를 확장해 값을 추가하는 방법
- 확장하여 값을 추가할 때 compareTo 규약을 지킬 방법이 없다.
- 컴포지션을 사용하자
# compareTo 메서드 작성 요령
- == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
- instanceof 연산자로 입력이 올바른 타입인지 확인한다.
- 입력을 올바른 타입으로 형변환한다.
- 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사한다.
# equals와 차이점
- Comparable 은 타입을 인수로 받는 제네릭 인터페이스
- compareTo 메서드의 인수 타입은 컴파일 타임에 정해짐
- 입력 인수타입을 확인하거나 형변환 할 필요가 없다.
- null 을 인수로 넣어 호출하면 NullPointerException 을 던져야 한다.
# 객체 compareTo 의 비교
compareTo 는 각 필드의 동치를 비교하는 것이 아니라, 순서를 비교함
- 객체참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출함
- Comparator 비교자의 사용
- Comparable 을 구현하지 않은 필드
- 표준이 아닌 순서로 비교해야 할 때
- 비교자는 직접 만들거나 자바가 제공하는 것 중에 고르면 된다.
- 다음 코드는 자바가 제공하는 비교자를 사용하였음
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> { public int compareTo(CaseInsensitiveString cis) { return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s); } }
1
2
3
4
5
6implements Comparable<CaseInsensitiveString>
- CaseInsensitiveString 의 참조는, CaseInsensitiveString 참조와만 비교할 수 있다
# 기본 타입의 비교
자바7 이후의, 박싱된 기본 타입 클래스들에 추가된 정적 메서드 compare 사용
# 비교 순서
클래스에 핵심필드가 여러개라면 비교 순서가 중요하다
# 1. 가장 핵심적인 필드부터 비교
- 비교 결과가 0 이 아니라면, (순서가 결정되면) 결과를 바로 반환해야 한다.
- 핵심 필드가 똑같다면(0), 똑같지 않은 필드를 찾을 때 까지 그 다음으로 중요한 필드를 비교한다.
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;
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 비교자 생성 메서드를 활용한 비교자
자바8 에서, Comparator 인터페이스가 비교자 생성 메서드 comparator constructor method 와 메서드 연쇄방식으로 비교자를 생성할 수 있게 되었다.
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);
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 특징
- 약간의 성능저하
- 정적 비교자 생성 메서드들을 이름으로만 사용할 수 있어 코드가 깔끔해짐
# 정적 compare 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hasCode(), o2.hashCode());
}
}
1
2
3
4
5
2
3
4
5
# 비교자 생성 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder =
Comparator.comparingInt(o -> o.hashCode());
1
2
2