1. Comparable 인터페이스의 유일한 메서드 compareTo
compareTo 는 단순 동치성 비교 + 순서 비교가 가능하다. compareTo 를 구현했다는 것은, 순서가 존재하다는 것이고, Arrays.sort 를 활용한 정렬이 가능하다는 것이다. String 이 CompareTo 를 구현한 덕분에, 자동 정렬되는 TreeSet 자료구조 상, 출력하면 알파벳순으로 정렬되어 출력된다.
public class Main {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
}
2. compareTo 일반 규약
객체 A와 객체 B의 순서를 비교한다. A객체가 B객체보다 작으면 음수, 같으면 0, 크면 양수를 반환허고, 비교할 수 없는 객체 타입이면 ClassCastException 을 던진다.
아래 설명에서 sgn은 부호함수를 뜻하며, 표현식의 값이 음수, 0, 양수일 때 -1, 0, 1을 반환하도록 정의했다.
Comparable을 구현한 클래스는 대칭성을 보장해야 한다.
Comparable을 구현한 클래스는 모든 x, y에 대하여 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))여야 한다.
두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야한다. 첫번째 객체 > 두번째 객체면, 두번째 객체 < 첫번째 객체 여야 한다.
Comparable을 구현한 클래스는 추이성을 보장해야 한다.
즉, (x.compareTo(y) > 0 && y.compareTo(z) > 0)이면 x.compareTo(z) > 0 이다.
첫번째 객체가 두번째 객체보다 크고, 두번째 객체가 세번째 객체보다 크면, 첫번째 객체는 세번째 객체보다 커야한다.
Comparable을 구현한 클래스는 반사성을 보장해야 한다.
Comparable을 구현한 클래스는 모든 z에 대해
x.compareTo(y) == 0이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))이다.
크기가 같은 객체들 간에는 어떤 객체와 비교하더라도 같아야 한다.
(필수는 아니지만 지키는게 좋음)
(x.compareTo(y) == 0) == (x.equals(y))여야 한다.
compareTo 메서드로 수행한 동치성 결과가, equals 와 같아야 한다. 이를 잘 지키면, compareTo 에 의해 줄지어진 순서와 equals 의 결과가 동일하게 되어 정렬된 컬렉션에서도 확실하게 사용할 수가 있게된다. compareTo 와 equals 가 일관되지 않는 BigDecimal 클래스를 예시로 확인해보자.
final BigDecimal bigDecimal1 = new BigDecimal("1.0");
final BigDecimal bigDecimal2 = new BigDecimal("1.00");
final HashSet<BigDecimal> hashSet = new HashSet<>();
hashSet.add(bigDecimal1);
hashSet.add(bigDecimal2);
System.out.println(hashSet.size());
final TreeSet<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(bigDecimal1);
treeSet.add(bigDecimal2);
System.out.println(treeSet.size());
// 실행결과
hashSet: 2
treeSet: 1
compareTo 메서드로 비교하면 두 BigDecimal 인스턴스가 동일하기 때문에, 원소를 하나만 갖게 된다.
compareTo 규약을 지키지 않는다면, 비교를 활용하는 TreeSet, TreeMap, Collectionsm Arrays 등과 어울릴 수 없다.
3. compareTo 작성 요령
클래스에 핵심 필드가 여러개라면, 가장 핵심적인 필드부터 비교하자. 가장 핵심적인 필드가 같다면, 똑같지 않은 필드를 찾아나갈 때 까지 그 다음으로 중요한 필드를 비교하고, 비교 결과가 0이 아니라면 곧장 반환하자.
public int compare(final PhoneNumber phoneNumber) {
int result = Short.compare(areaCode, phoneNumber.areaCode); // 가장 중요
if (result == 0) {
result = Short.compare(prefix, phoneNumber.prefix); // 그 다음 중요
if (result == 0) {
result = Short.compare(lineNum, phoneNumber.lineNum); // 다다음 중요
}
}
return result;
}
4. Comparator
자바 8 이후, Comparator 인터페이스 (Comparable 유사품) 가 일련의 비교자 생성 메서드와 함께 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다. 이를 통해 compareTo 메서드를 구현할 수 있는데, 예시를 통해 살펴보자.

private static final Comparator<PhoneNumber> COMPARATOR =
Comparator.comparingInt((PhoneNumber phoneNumber) -> phoneNumber.areaCode)
.thenComparingInt(phoneNumber -> phoneNumber.prefix)
.thenComparingInt(phoneNumber -> phoneNumber.lineNum);
public int compareTo(PhoneNumber phoneNumber) {
return COMPARATOR.compare(this, phoneNumber);
}
성능은 살짝 뒤떨어지지만, 코드가 훨씬 깔끔해진다. 비교자 생성 메서드인 comparingInt 를 통해 지역 코드를 비교하고, 같을 경우를 대비해 thenComparingInt 를 수행해 프리픽스, 가입자 번호를 연달아 호출하며 비교한다.
또한, comparator 인터페이스는 수많은 보조 생성 매서드들이 있다. long 과 double 을 대비해 comparingInt 와 thenComparingInt 의 변형 메서드들이 있다.
Comparator 인터페이스를 활용한 정적 compare 메서드, 비교자 생성 메서드를 활용한 '값의 차' 비교자 예시이다.
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
@Override
public int compare(final Object o1, final Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};
static Comparator<Object> hashCodeOrder =
Comparator.comparingInt(o -> o.hashCode());
5. 결론
순서를 고려해야 하는 값 클래스를 작성한다면, 꼭 Comparable 인터페이스를 구현하여, 정렬/검색/비교 기능을 제공하는 컬렉션과 어우러지도록 하자.
compareTo 메서드에서 필드 값 비교 시, '<' '>' 연산자 대신 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
'1️⃣ 백앤드 > 이펙티브 자바' 카테고리의 다른 글
| [아이템16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2024.10.05 |
|---|---|
| [아이템15] 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2024.10.04 |
| [아이템13] clone 재정의는 주의해서 진행하라 (0) | 2024.09.25 |
| [아이템12] toString 을 항상 재정의하라 (1) | 2024.09.25 |
| [아이템11] equals 를 재정의하려거든 hashCode 도 재정의하라 (3) | 2024.09.24 |