Comparable 인터페이스의 메서드인 compareTo
compareTo는 단순 동치성과 순서의 비교가 가능하며 제네릭하다.
Comparable을 구현했다는 것은 해당 클래스의 인스턴스들에는 자연적인 순서가 있음을 뜻하기에 Comparable을 구현한 객체들의 배열은 아래와 같이 쉽게 정렬할 수 있다.
Arrays.sort(a);
검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게 할 수 있다.
String이 Comparable을 구현한 덕분에 아래에서 중복 제거 및 알파벳 순 출력이 가능하다.
public class WordList {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
}
자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입(Item 34)이 Comparable을 구현했다. 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.
🍑 본론
public interface Comparable<T> {
int compareTo(T t);
}
compareTo 메서드의 일반 규약
이 객체와 주어진 객체의 순서 비교
이 객체가 주어진 객체보다 작으면 음의 정수, 같으면 0, 크면 양의 정수를 반환한다.
비교 불가능한 타입의 객체가 주어지면 ClassCastException을 던진다.
다음의 sgn(표현식) 표기는 수학에서 말하는 부호함수(signum function)를 뜻하며, 표현식의 값이 음수, 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))다.
-> "크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다."
위 세 규약은 compareTo 메서드로 수행하는 동치성 검사도 equals 규약과 똑같이 반사성, 대칭성, 추이성을 충족해야함을 뜻한다. 따라서 주의사항도, 우회법도 같다.(Item 10)
기존 Comparable을 구현한 클래스를 확장해 새로운 값 컴포넌트를 추가하고 싶다면,
확장하는 대신 독립된 클래스를 만들고 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두면 된다.
그리고 내부 인스턴스를 반환하는 '뷰' 메서드를 제공하면 된다.
(권고사항) (x.compareTo(y) == 0) == (x.equals(y))여야 한다. Comparable을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야 하며 명시의 예시는 다음과 같다.
"주의: 이 클래스의 순서는 equals 메서드와 일관되지 않는다."-> "compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다."
해당 규약을 지키면 compareTo로 정렬한 순서와 equals의 결과가 일관되게 된다. compareTo의 순서와 equals의 결과가 일관되지 않은 클래스도 여전히 동작하지만 이 클래스의 객체를 정렬된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스(Collection, Set, Map)에 정의된 동작과 엇박자를 낼 것이다. 이 인터페이스들은 equals 메서드의 규약을 따른다고 되어 있지만, 정렬된 컬렉션들은 동치성을 비교할 때 equals 대신 compareTo를 사용하기 때문에 주의해야 한다.
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
Set<BigDecimal> hSet = new HashSet<>();
hSet.add(a);
hSet.add(b);
Set<BigDecimal> tSet = new TreeSet<>();
tSet.add(a);
tSet.add(b);
System.out.println("equal 비교 : " + a.equals(b));
System.out.println("compareTo 비교 : " + a.compareTo(b));
System.out.println("HashSet의 원소 개수 : " + hSet.size());
System.out.println("TreeSet의 원소 개수 : " + tSet.size());
// equal 비교 : false
// compareTo 비교 : 0
// HashSet의 원소 개수 : 2
// TresSet의 원소 개수 : 1
### compareTo 메서드 작성
compareTo 메서드는 각 필드의 순서를 비교한다.
객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출해야 한다.
Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 비교자(Comparator)를 대신 사용한다. 비교자는 직접 만들거나 자바가 제공하는 것 중 골라 사용하면 된다. 아래 코드는 자바가 제공하는 비교자를 사용한다.
(Item 10의 코드 참조)
```java
// 기본 타입 필드가 하나뿐인 비교자
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
@Override
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
}
이전에는 정수 기본 타입 필드를 비교할 때 관계연산자 <, >와 실수 기본 타입 필드를 비교할 때는 정적 메서드인 Double.compare, Float.compare를 사용하는 것을 권했다. 하지만 Java7부터 박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare를 이용하면 된다. 관계연산자 <, >를 사용하는 방식은 거추장스럽고 오류를 유발하기에 추천하지 않는다.
클래스에 핵심 필드가 여러 개라면 비교 순서가 중요해진다.(Item 10의 코드 참조)
// 기본 타입 필드가 여럿일 때의 비교자
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;
}
메서드 연쇄 방식으로 비교자를 생성하여 compareTo 메서드를 구현할 수 있다. (간결하지만 약간의 성능 저하가 뒤따른다. PhoneNumber 인스턴스의 정렬된 배열에 적용해보니 10%정도 느려졌다. -> 이거 이해안됨)
자바의 정적 임포트 기능을 이용하면 정적 비교자 생성 메서드들을 이름만으로 사용할 수 있어 코드가 훨씬 깔끔해진다.
// 비교자 생성 메서드를 활용한 비교자
import static java.util.Comparator.comparingInt; // import 추가
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);
}
```java
// 해시코드 값의 차를 기준으로 하는 비교자 - 추이성 위배
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
위 방식은 정수 오버플로우를 일으키거나 IEEE 754 부동소수점 계산 방식에 따른 오류를 낼 수 있다.
따라서 다음 두 방식 중 하나를 사용하는 것이 좋다.
// 정적 compare 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};
// 비교자 생성 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder =
Comparator.comparingInt(o -> o.hashCode());"
🍑 결론
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다. compareTo 메서드에서 필드의 값을 비교할 때 <와 > 연산자 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성메서드를 사용한다.
Chapter : 3. 모든 객체의 공통 메서드
Item : 14. Comparable을 구현할지 고려하라.
Assignee : heon118
🍑 서론
Comparable 인터페이스의 메서드인 compareTo compareTo는 단순 동치성과 순서의 비교가 가능하며 제네릭하다. Comparable을 구현했다는 것은 해당 클래스의 인스턴스들에는 자연적인 순서가 있음을 뜻하기에 Comparable을 구현한 객체들의 배열은 아래와 같이 쉽게 정렬할 수 있다.
검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게 할 수 있다. String이 Comparable을 구현한 덕분에 아래에서 중복 제거 및 알파벳 순 출력이 가능하다.
자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입(Item 34)이 Comparable을 구현했다. 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.
🍑 본론
compareTo 메서드의 일반 규약
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 메서드와 일관되지 않는다."
-> "compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다."// equal 비교 : false // compareTo 비교 : 0 // HashSet의 원소 개수 : 2 // TresSet의 원소 개수 : 1
클래스에 핵심 필드가 여러 개라면 비교 순서가 중요해진다.(Item 10의 코드 참조)
메서드 연쇄 방식으로 비교자를 생성하여 compareTo 메서드를 구현할 수 있다. (간결하지만 약간의 성능 저하가 뒤따른다. PhoneNumber 인스턴스의 정렬된 배열에 적용해보니 10%정도 느려졌다. -> 이거 이해안됨) 자바의 정적 임포트 기능을 이용하면 정적 비교자 생성 메서드들을 이름만으로 사용할 수 있어 코드가 훨씬 깔끔해진다.
Comparator에는 많은 보조 생성 메서드들이 있다.
숫자용 기본 타입
comparingLong, thenComparingLong
comparingInt, thenComparingInt (short, int)
comparingDouble, thenComparingDouble (float, double)
객체 참조용 비교자 생성 메서드
comparing
thenComparing
위 방식은 정수 오버플로우를 일으키거나 IEEE 754 부동소수점 계산 방식에 따른 오류를 낼 수 있다. 따라서 다음 두 방식 중 하나를 사용하는 것이 좋다.
🍑 결론
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다. compareTo 메서드에서 필드의 값을 비교할 때
<
와>
연산자 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성메서드를 사용한다.Referenced by