💥 개요
compareTo는 equals, hashcode, toString과 다르게 Object의 메서드가 아닙니다. 그리고 equals와 2가지만 제외하면 거의 비슷한데, compreTo는 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭합니다. 즉 Compareble을 구현하게 된다면 그 클래스의 인스턴스에는 자연적인 순서가 있음을 뜻합니다. 그래서 Compareble을 구현한 객체의 배열은 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 인터페이스를 구현해야합니다.
🕹️compareTo 메서드의 일반규약
compareTo 메서드의 일반 규약은 equals의 일반 규약과 비슷합니다.
- 두 객체 참조의 순서를 바꿔 비교해도 같은 결과가 나와야 한다.
- 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크다면 첫 번째는 세 번째보다 커야한다.
- 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다.
즉 compareTo 메서드로 수행하는 동치성 검사도 equals의 규약과 똑같이 반사성, 대칭성, 추이성을 충족해야 합니다.
그래서 주의사항도 equals와 같습니다. 기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가했다면 확장 대신 독립된 클래스를 만들고, 이 클래스에 원래 클래스의 인스턴스를 가르키는 필드를 두고 내부 인스턴스를 반환하는 '뷰' 메서드를 구현하면 됩니다.
마지막 규약은 간단히 말하면 compareTo 메서드로 수행한 결과와 equals의 결과가 일관되어야 한다는 것 입니다. 일관되지 않아도 동작은 하지만 하지만 정렬된 컬렉션을 사용하면 잘못된 동작을 할 가능성이 커집니다.
또한 hashcode에서 일반규약을 지키지 않았을 때 문제점도 거의 흡사합니다. TreeSet이나 TreeMap에서 동일한 객체로 인식하지 못해 문제가 발생하는점이죠.
💡compareTo 작성 요령
작성 요령도 equals와 비슷합니다. 몇가지 차이점이 있는데 Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메서드의 인수 타입을 컴파일 시점에 정해지고, 입력 인수의 타입을 확인하거나 형변환 할 필요가 없습니다.
compareTo 메서드는 각 필드가 동치인지 비교하는게 아니라 순서를 비교합니다.
객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출합니다.
Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 비교자(Comparator)를 대신 사용합니다. 비교자는 직접 만들거나 자바에서 재공하는 것을 사용할 수 있습니다.
public final class CaseInsensitiveString
implements Comparable<CaseInsensitiveString> {
private final String s;
public int compareTo(CaseInsensitiveString cis) {
//JAVA에서 제공하는 비교자
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
}
CaseInsensitiveString은 Comparable<CaseInsensitiveString>를 구현하는데 CaseInsensitiveString의 참조는 CaseInsensitiveString참조와만 비교할 수 있다는 뜻으로 Comparable을 구현할 때 일반적으로 따르는 패턴입니다.
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;
}
위 코드처럼 중요한 필드부터 0이 아니라면 비교하는 방식으로 구현하는게 좋습니다.
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);
}
자바 8에서 Comparator 인터페이스가 일련의 비교자 생성 메서드와 팀을 꾸려 메서드 체이닝 방식으로 비교자를 생성할 수있게 되어 compareTo 메서드를 비교하는데 활용할 수 있게 되었습니다. 하지만 책 기준에서 10% 정도 성능이 떨어지지만 훨씬 코드가 깔끔해집니다.
//1.해시코드 값의 차를 기준으로 하는 비교자 - 추이성 위배!!
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
//2.정적 compare 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};
//3.비교자 생성 메서드를 활용한 비교자
static Comparator<Obejct> hashCodeOrder =
Comparator.compareingInt(o -> o.hashCode());
첫 번째 방식은 정수 오버플로를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있고 속도도 월등히 빠르지 않습니다. 그 대신 2번이나 3번의 방법을 통해 비교해야 합니다.
❤️ 스터디 질답 정리
Q. 왜 < > 로 비교하면 거추장스럽고 오류를 유발하는가?
A. 부등호를 쓰는건 한번 더 계산이 들어가기 때문에 정적 compare 메서드나 비교자 생성 메서드를 활용하자.
Q. compare과 equals 결과가 같아야 하는 이유가 있을까?
A. BigDecimal 클래스의 경우 equals와 compareTo의 결과가 다를 수 있도록 구현되어 있는데 컬렉션에서 equals로 비교하고, compareTo로 비교를 하기 때문에 컬렉션을 사용할 때 일치 시키는게 좋다.
추가로, 정확하게 treeSet이나 treeMap에서 equals를 통해 비교를 하기 때문에 문제가 발생할 수 있다.
Q.Comparable, Comparator의 차이점?
A.Comparable은 기본적으로 클래스의 자연적인 순서를 정의한 것, 그 외에 추가적으로 정렬하고 싶으면 Comparator, Comparable는 함수형 인터페이스고 매개변수가 2개, Comparator는 매개변수가 1개
만약 Comparable을 구현하지 않았고 TreeSet이나 TreeMap을 사용하려면 Comparator를 사용해야함
Q. 관계 연산자를 사용했을 때 거추장스러운건 알겠는데 무슨 오류를 유발하는건 뭘 얘기하는건가?
A. 관계 연산자를 사용하게 되면 null인 경우에 같다라고 0이 반환되는데 compare를 사용하면 에러가 발생한다.
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다. compareTo 메서드에서 필드의 값을 비교할 때 <와 > 연산자는 쓰지 말아야 한다. 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
'Study > 이펙티브 자바' 카테고리의 다른 글
[Effective Java 3E] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2023.09.09 |
---|---|
[Effective Java 3E] 클래스와 멤버의 접근 권한을 최소화하라 (2) | 2023.09.09 |
[Effective Java 3E] clone 재정의는 주의해서 진행하라 (0) | 2023.09.01 |
[Effective Java 3E] toString을 항상 재정의하라 (0) | 2023.09.01 |