-
Item 14. Comparable을 구현할지 고려하라Book/Effective Java 3E 2022. 10. 20. 10:30반응형
이번 아이템에서는 Comparable 인터페이스의 유일무이한 메서드인 compareTo에 대해 알아보겠습니다. 성격은 두 가지만 빼면 Object의 equals와 같습니다. compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭합니다. Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서(natural order)가 있음을 뜻합니다. 그래서 Comparable을 구현한 객체들의 배열은 다음처럼 손쉽게 정렬할 수 있습니다.
Arrays.sort(a);
검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게 할 수 있습니다. 사실상 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입(Item 34)이 Comparable을 구현했습니다. 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현합시다!
public interface Comparable<T> { int compareTo(T t); }
compareTo 메서드의 일반 규약은 equals의 규약과 비슷합니다.
이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 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))다.
* 이번 권고가 필수는 아니지만 꼭 지키는 게 좋다. (x.compareTo(y) == 0) == (x.equals(y))여야 한다. Comparable을 구현하고 이 권고
를 지키지 않는 모든 클래스는 그 사실을 명시해야 한다. 다음과 같이 명시하면 적당할 것이다.
"주의: 이 클래스의 순서는 equals 메서드와 일관되지 않다."hashCode 규약을 지키지 못하면 해시를 사용하는 클래스와 어울리지 못하듯, compareTo 규약을 지키지 못하면 비교를 활용하는 클래스와 어울리지 못합니다. 비교를 활용하는 클래스의 예로는 정렬된 컬렉션인 TreeSet과 TreeMap, 검색과 정렬 알고리즘을 활용하는 유틸리티 클래스인 Collections와 Arrays가 있습니다.
compareTo와 equals가 일관되지 않은 BigDecimal 클래스를 예로 보겠습니다. 빈 HashSet 인스턴스를 생성한 다음 new BigDecimal("1.0")과 new BigDecimal("1.00")을 차례로 추가합니다. 이 두 BigDecimal은 equals 메서드로 비교하면 서로 다르기 때문에 HashSet은 원소를 2개 갖게 됩니다. 하지만 TreeSet을 사용하면 원소를 하나만 갖게 됩니다. compareTo 메서드로 비교하면 두 BigDecimal 인스턴스가 똑같기 때문입니다(자세한 설명은 BigDecimal 문서를 참고하시면 됩니다).
코드 14-2은 Item 10의 PhoneNumber 클래스용 compareTo 메서드를 구현한 모습입니다. 자바 8에서는 Comparator 인터페이스가 일련의 비교자 생성 메서드(comparator construction method)와 팀을 꾸려 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었습니다. 그리고 이 비교자들을 Comparable 인터페이스가 원하는 compareTo 메서드를 구현하는 데 멋지게 활용할 수 있습니다(코드 14-3). 코드가 훨씬 깔끔해지지만, 약간의 성능 저하가 뒤따릅니다.
package effectivejava.chapter3.item14; import java.util.*; import java.util.concurrent.ThreadLocalRandom; import static java.util.Comparator.*; // PhoneNumber를 비교할 수 있게 만든다. 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, "가입자 번호"); } ... 생략 ... // // 코드 14-2 기본 타입 필드가 여럿일 때의 비교자 // 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; // } // 코드 14-3 비교자 생성 메서드를 활용한 비교자 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); } private static PhoneNumber randomPhoneNumber() { Random rnd = ThreadLocalRandom.current(); return new PhoneNumber((short) rnd.nextInt(1000), (short) rnd.nextInt(1000), (short) rnd.nextInt(10000)); } public static void main(String[] args) { NavigableSet<PhoneNumber> s = new TreeSet<PhoneNumber>(); for (int i = 0; i < 10; i++) s.add(randomPhoneNumber()); System.out.println(s); } }
Comparator는 long과 double용으로는 comparingInt와 thenComparingInt의 변형 메서드를 준비했습니다. short처럼 더 작은 정수 타입에는 int용 버전을 사용하면 됩니다. 마찬가지로 float은 double용을 이용해 수행합니다. 이런 식으로 자바의 숫자용 기본 타입을 모두 커버합니다. 객체 참조용 비교자 생성 메서드도 준비되어 있습니다. comparing이라는 정적 메서드 2개가 다중정의되어 있으며, thenComparing이란 인스턴스 메서드가 3개 다중정의되어 있습니다.
'값의 차'를 기준으로 하는 비교자 방식 대신 정적 compare 메서드를 활용한 비교자나 비교자 생성 메서드를 활용한 비교자 방식을 사용합시다!
// 해시코드 값의 차를 기준으로 하는 비교자 - 추이성을 위배한다! static Comparator<Object> hashCodeOrder = new Comparator<>() { public int compare(Object o1, Object o2) { return o1.hashCode() - o2.hashCode(); } }; // 정적 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 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
[참고 정보]
이펙티브 자바 Effective Java 3/E 도서 [조슈아 블로크 저]
반응형'Book > Effective Java 3E' 카테고리의 다른 글
Item 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) 2022.10.21 Item 15. 클래스와 멤버의 접근 권한을 최소화하라 (0) 2022.10.21 Item 13. clone 재정의는 주의해서 진행하라 (0) 2022.10.19 Item 12. toString을 항상 재정의하라 (0) 2022.10.18 Item 11. equals를 재정의하려거든 hashCode도 재정의하라 (2) 2022.10.18