Comparable
인터페이스의 compareTo
는, 객체를 비교할 수 있도록 만드는 메서드이다.
즉 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다는 두 가지를 제외하면 equals
와 동일하게 동작한다. 당연히, compareTo
메서드로 수행한 동치성 테스트의 결과가 equals
와 같아야 한다.
public interface Comparable<T> {
int compareTo(T t);
}
compareTo
는 주어진 객체의 순서를 비교하는 메서드이다.
작은
경우 : 음수 반환(-1)같은
경우 : 0 반환큰
경우 : 양수 반환(1)사실상, 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입들은 모두 Comparable
인터페이스를 구현하고 있다.
그렇다면 정렬을 사용하는 컬렉션에서 어떤 방식으로 사용할까?
private static void mergeSort(Object[] src, Object[] dest, int low, int high, int off) {
int length = high - low;
// Insertion sort on smallest arrays
if (length < INSERTIONSORT_THRESHOLD) {
for (int i=low; i<high; i++)
for (int j=i; j>low &&
((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
swap(dest, j, j-1);
return;
}
// Recursively sort halves of dest into src
int destLow = low;
int destHigh = high;
low += off;
high += off;
int mid = (low + high) >>> 1;
mergeSort(dest, src, low, mid, -off);
mergeSort(dest, src, mid, high, -off);
}
위 코도를 보면, 인접한 두 객체의 compareTo
결과가 0 이상이라면 두 객체의 순서를 뒤집는다. 기본값이 오름차순인 정렬이므로, 당연히 현재 객체가 주어진 객체보다 큰 경우 양수를 반환한다 했으니 스왑이 일어난다.
참고로 해당 사실을 이용하면, 내림차순 정렬 시에는 해당 객체가 주어진 객체보다 큰 경우 스왑을 하지 않도록 음수를 반환하도록 하면 된다.
정렬에서 동치성을 비교할 때 equals
가 아닌 compareTo
를 사용하니, equals
규약을 똑같이 만족해야 한다는 것을 볼 수 있다.
예시로 BigDecimal
은 compareTo
와 equals
가 일관되지 않는다. 이유는 compareTo
에서 값은 같지만 배율이 다른 두 개의 BigDecimal
개체(예: 2.0 및 2.00)는 동일한 것으로 간주되기 때문이다.
@Test
void test1() throws IOException {
BigDecimal b1 = new BigDecimal("1.0");
BigDecimal b2 = new BigDecimal("1.00");
Set hashSet = new HashSet();
hashSet.add(b1);
hashSet.add(b2); // 2개
System.out.println("hashSet size: " + hashSet.size());
Set treeSet = new TreeSet();
treeSet.add(b1);
treeSet.add(b2);
System.out.println("treeSet size: " + treeSet.size());
}
🔖 상속과
compareTo
기존 클래스를 확장한 하위 클래스에서 새로운 필드가 추가되었다면, 아이템 10과 마찬가지로 객체지향을 택하는 대신compareTo
규약을 지킬 수 없다. 이런 경우, 확장하는 대신 독립된 클래스를 생성하고 원래 클래스 인스턴스를 가리키는 필드를 두면 우회할 수 있다.(컴포지션)
타입을 인수로 받는 제네릭 인터페이스이다.
compareTo
메서드의 인수 타입은 컴파일 타임에 정해지므로, 입력 인수의 타입을 확인하거나 형변환할 필요가 없다.
null
을 인수로 넣어 호출하면, nullPointException
을 던저야 한다.
객체 참조 필드를 비교하는 경우
compareTo
메서드를 재귀적으로 호출한다. Comparable
을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 Comparator
을 사용하자.
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
compare
을 사용한다.public final class Integer extends Number implements Comparable<Integer> {
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
}
public final class PhoneNumber implements Cloneable, Comparable<PhoneNumber> {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
}
//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이 아니라면 순서가 결정됌
}
}
자바 8부터는 Compartor
인터페이스가 비교자 생성 메서드와 팀을 꾸려, 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다. 하지만 약간의 성능 저하가 뒤따르긴 한다.
@FunctionalInterface
public interface Comparator<T> {
default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}
...
}
comparingInt()
: 어떤 값을 기준으로 정렬할 것인지 인자로 전달받아서, 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드 private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix) //1
.thenComparingInt(pn -> pn.lineNum); //2
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
값의 차를 기준으로 결과를 반환하는 compareTo
나 compare
메서드는, 정수 오버플로를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있으니 사용하면 안된다.
예를 들어, o1
= 1
, o2
= -2,147,483,648
라고 가정하자. 두 수를 return o1 - o2;
형식으로 반환한다면, 1 - (-2,147,483,648) = 2,147,483,649
로 양수가 나와야 하는데 -2,147,483,648
으로 음수가 나와 1이 더 작다는 상황이 발생하게 된다.
static Comparator<Object> hasCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2){
return o1.hashCode() - o2.hashCode(); // 해시코드 기반 차이 반환
}
};
static Comparator<Object> hasCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2){
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};
static Comparator<Object> hasCodeOrder =
Comparator.comparingInt(o -> o.hashCode()); // 해시코드 값 기반
💡 핵심 정리
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다.compareTo
메서드에서 필드의 값을 비교할때<
와>
연산자를 사용하지 말아야 한다. 대신, 박싱된 기본 타입 클래스가 제공하는 정적compare
메서드나Comparator
인터페이스가 제공하는 비교자 생성 메서드를 사용하자.