※ compareTo는 Object의 메서드가 아니다. (equals, hashCode, toString, clone은 Object의 메서드)
※ compareTo 메서드 : 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다.
객체와 비교할 수 없는 객체가 주어지면 ClassCastException을 던진다.
[Why]
- compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다.
- Comparable을 구현한 객체들의 배열은 손쉽게 정렬할 수 있다. e.g.) Arrays.sort(a);
[When]
- 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.
[How]
equals 규약과 똑같이 반사성, 대칭성, 추이성을 충족해야 한다.
- equals와 주의사항도 똑같고, 우회법도 같다.
- 기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가했다면 compareTo 규약을 지킬 방법이 없다아이템10.
public class Point implements Comparable<Point> {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public int compareTo(Point o) {
if(o.x > this.x && o.y > this.y) {
return -1;
} else if(o.x < this.x && o.y < this.y) {
return 1;
}
return 0;
}
}
public class ColorPoint extends Point {
private final Color color; // String 값
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public int compareTo(Point o) {
?????
}
... // 나머지 코드는 생략
}
- 기존 클래스를 확장한 구체 클래스에서 compareTo를 구현했을 때의 문제점
- 상위 클래스의 좌표값 x, y만 비교하면 color값을 비교할 수 없다.
- 하위 클래스의 color값까지 넣어서 비교하면 상위 클래스의 인스턴스가 비교대상으로 들어왔을 때 color값이 없어서 에러난다.
- 구현체의 compareTo에서 인자로 ColorPoint만 받으면 리스코프 치환 원칙에 위배된다.
- Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면, 독립된 클래스를 만들고 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두자.
public class CompositePoint implements Comparable<CompositePoint> {
private final Point point;
private final Color color;
public CompositePoint(Point point, Color color) {
this.point = point;
this.color = color;
}
public Point getPoint() {
return point;
}
public Color getColor() {
return color;
}
@Override
public int compareTo(CompositePoint o) {
// 비교 로직
}
}
- 그 다음 내부 인스턴스를 반환하는 '뷰' 메서드를 제공하면 된다.
- 이렇게 하면 바깥 클래스(CompositePoint)에 우리가 원하는 compareTo 메서드를 구현해넣을 수 있다.
- 클라이언트는 필요에 따라 바깥 클래스의 인스턴스를 필드 안에 담긴 원래 클래스의 인스턴스로 다룰 수도 있다.(??)
마지막 규약은 이왕이면 지키도록 한다. (x.compareTo(y) == 0) == (x.equals(y))
- 이를 잘 지키면 compareTo로 줄지은 순서와 equals의 결과가 일관되게 된다.
- 만약 일관되지 않으면, 이 클래스의 객체를 정렬된 컬렉션에 넣었을 때 해당 컬렉션이 구현한 인터페이스(Collection, Set, Map)에 정의된 동작과 엇박자를 낼 것이다.
- 이 인터페이스들은 equals 메서드의 규약을 따른다고 되어 있지만, 정렬된 컬렉션들은 동치성을 비교할 때 compareTo를 사용한다.
- new BigDecimal("1.0"), new BigDecimal("1.00")
- HashSet에 넣으면 equals 메서드로 비교해 원소가 2개가 된다.
- TreeSet에 넣으면 compareTo 메서드로 비교해 원소가 1개가 된다.
Comparable은 제네릭 인터페이스이므로 compareTo 메서드의 인수 타입은 컴파일타임에 정해진다.
- 입력 인수의 타입을 확인하거나 형변환할 필요가 없다는 뜻이다.
Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 Comparator를 사용한다.
- compareTo 메서드에서 필드의 값을 비교할 때 '<', '>' 연산자는 쓰지 말아야 한다.
- 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
클래스에 핵심 필드가 여러 개라면 가장 핵심적인 필드부터 비교해나가자.
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;
}
'값의 차'를 기준으로 비교하지 말자.
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
}
- 정수 오버플로를 일으키거나, 부동소수점 계산 방식에 따른 오류를 낼 수 있다. 그리고 월등히 빠르지도 않다.
- 정적 compare 메서드나 비교자 생성 메서드를 활용하자.
TIP
자바 8에서의 비교자 생성 메서드(comparator construction method)
- Comparator 인터페이스가 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다.
- 이 비교자들을 Comparable 인터페이스의 compareTo 메서드를 구현하는 데 사용할 수 있다.
- 하지만 약간의 성능 저하가 뒤따른다. (저자 기준 10% 느려짐)
- 자바의 정적 임포트 기능을 이용하면 정적 비교자 생성 메서드들을 그 이름만으로 사용할 수 있어 코드가 훨씬 깔끔해진다.
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);
}
- 이 코드는 클래스를 초기화할 때 비교자 생성 메서드 2개(comparingInt, thenComparingInt)를 이용해 비교자를 생성한다.
- comparingInt는 객체 참조를 int 타입 키에 매핑하는 키 추출 함수(key extractor function)를 인수로 받아, 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드다.
- 위의 예에서 comparingInt는 람다(lambda)를 인수로 받으며, 이 람다는 PhoneNumber에서 추출한 지역 코드를 기준으로 전화번호의 순서를 정하는 Comparator<PhoneNumber>를 반환한다.
- thenComparingInt는 Comparator의 인스턴스 메서드로, int 키 추출자 함수를 입력 받아 다시 비교자를 반환한다.
- comparingInt는 객체 참조를 int 타입 키에 매핑하는 키 추출 함수(key extractor function)를 인수로 받아, 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드다.
Comparator의 객체 참조용 비교자 생성 메서드
- comparing 정적 메서드가 2개, thenComparing이란 인스턴스 메서드가 3개 다중정의되어 있다.
-
키 추출자를 받아서 그 키의 자연적 순서를 사용한다.public static <T, U extends Comparable<? super U>> Comparator<T> comparing( Function<? super T, ? extends U> keyExtractor) { ... // 생략 }
-
키 추출자 하나와 추출된 키를 비교할 비교자까지 총 2개의 인수를 받는다.public static <T, U> Comparator<T> comparing( Function<? super T, ? extends U> keyExtractor, Comparator<? super U> keyComparator) { ... // 생략 }
-
비교자 하나만 인수로 받아 그 비교자로 부차(副次) 순서를 정한다.default Comparator<T> thenComparing(Comparator<? super T> other) { ... // 생략 }
-
키 추출자를 인수로 받아 그 키의 자연적 순서로 보조 순서를 정한다.default <U extends Comparable<? super U>> Comparator<T> thenComparing( Function<? super T, ? extends U> keyExtractor) { ... // 생략 }
-
키 추출자 하나와 추출된 키를 비교할 비교자까지 총 2개의 인수를 받는다.default <U> Comparator<T> thenComparing( Function<? super T, ? extends U> keyExtractor, Comparator<? super U> keyComparator) { ... // 생략 }
-
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2022.01.08 |
---|---|
[아이템 15] 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2022.01.08 |
[아이템 13] clone 재정의는 주의해서 진행하라 (0) | 2022.01.08 |
[아이템 12] toString을 항상 재정의하라 (0) | 2022.01.08 |
[아이템 11] equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2022.01.08 |