-
[이펙티브 자바] item 14 - Comparable을 구현할지 고려하라개발서적읽기/Effective Java - temp 2020. 7. 9. 00:28
■Comparable 인터페이스
public interface Comparable<T> {
/**
* Compares this object with the specified object for order. Returns a
* negative integer, zero, or a positive integer as this object is less
* than, equal to, or greater than the specified object.
*
* <p>The implementor must ensure <tt>sgn(x.compareTo(y)) ==
* -sgn(y.compareTo(x))</tt> for all <tt>x</tt> and <tt>y</tt>. (This
* implies that <tt>x.compareTo(y)</tt> must throw an exception iff
* <tt>y.compareTo(x)</tt> throws an exception.)
*
* <p>The implementor must also ensure that the relation is transitive:
* <tt>(x.compareTo(y)>0 && y.compareTo(z)>0)</tt> implies
* <tt>x.compareTo(z)>0</tt>.
*
* <p>Finally, the implementor must ensure that <tt>x.compareTo(y)==0</tt>
* implies that <tt>sgn(x.compareTo(z)) == sgn(y.compareTo(z))</tt>, for
* all <tt>z</tt>.
*
* <p>It is strongly recommended, but <i>not</i> strictly required that
* <tt>(x.compareTo(y)==0) == (x.equals(y))</tt>. Generally speaking, any
* class that implements the <tt>Comparable</tt> interface and violates
* this condition should clearly indicate this fact. The recommended
* language is "Note: this class has a natural ordering that is
* inconsistent with equals."
*
* <p>In the foregoing description, the notation
* <tt>sgn(</tt><i>expression</i><tt>)</tt> designates the mathematical
* <i>signum</i> function, which is defined to return one of <tt>-1</tt>,
* <tt>0</tt>, or <tt>1</tt> according to whether the value of
* <i>expression</i> is negative, zero or positive.
*
* @param o the object to be compared.
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
*
* @throws NullPointerException if the specified object is null
* @throws ClassCastException if the specified object's type prevents it
* from being compared to this object.
*/
public int compareTo(T o);
}객체간의 비교를 용이하게 해주는 인터페이스이다.
이 인터페이스를 구현한 객체들은 서로간의 비교 연산이 가능하다.
(아래에 등장하는 구현 규약을 지킨다는 전제 하에)
또한 이러한 객체들은 자연적인 순서(natural order)가 있음을 뜻하게 된다.
이러한 특징 덕분에 비교 연산이 포함된 여러 자바 라이브러리들은
이 인터페이스를 구현한 객체들에게 Arrays.sort이나 Collections.addAll 등의
다양한 기능들을 제공한다.
사실상 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입(item 34)이
Comparable을 구현했다. 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 정의한다면
반드시 Comparable 인터페이스를 구현하자.
■compareTo
compareTo 메서드를 작성할 때에는 몇 가지 지켜야 하는 규약이 있다.
1. 매개변수 인스턴스와 자신의 인스턴스를 비교하도록 해야 한다.
자신의 인스턴스가 매개변수 인스턴스보다 작으면 음의 정수를,
같으면 0을, 크면 양의 정수를 반환한다.
자신의 인스턴스와 비교할 수 없는 타입의 인스턴스가 주어지면
ClassCastException을 던진다.
2. 모든 x,y에 대해 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))여야 한다.
* sgn => 표현식의 값이 음수, 0, 양수일 때 각각 -1, 0, 1을 반환하는 함수
3. x.compareTo(y) > 0 && y.compareTo(z) > 0 이면
x.compareTo(z) > 0이다. 즉, 추이성을 보장해야 한다.
4. x.compareTo(y) == 0 이면
모든 z에 대해 sgn(x.compareTo(z)) == sgn(y.compareTo(z)) 이다.
5. (x.compareTo(y) == 0) == (x.equals(y))여야 한다.
이 규약을 지키지 않는 모든 클래스는 그 사실을 명시해야 한다.
예) "주의: 이 클래스의 순서는 equals 메서드와 일관되지 않다."
모든 객체에 대해 전역 동치관계를 부여하는 equals 메서드와 달리,
(모든 케이스에 대해 true or false를 return 해야 한다는 의미로 해석된다.)
compareTo는 타입이 다른 객체를 신경 쓰지 않아도 된다.
타입이 다른 객체가 주어지면 간단히 ClassCastException을 던져도 된다.
물론 다른 타입 사이의 비교도 허용하는데 보통은 비교할 객체들이 구현한
공통 인터페이스를 매개로 이뤄진다.
hashCode 규약을 지키지 못하면 해시를 사용하는 클래스와 어울리지 못하게 된다.
마찬가지로 compareTo 규약을 지키지 못하면
비교를 활용하는 클래스와 어울리지 못한다.
비교를 활용하는 클래스의 예로는 정렬된 컬렉션인 TreeSet과 TreeMap이 있고
검색과 정렬 알고리즘을 활용하는 유틸리티 클래스인 Collections와 Arrays가 있다.
위의 compareTo 마지막 규약은 필수는 아니지만 꼭 지키길 권한다.
마지막 규약은 간단히 말하면 compareTo 메서드로 수행한 동치성 테스트의
결과가 equals의 결과와 같아야 한다는 것이다.
이를 잘 지키면 compareTo로 줄지은 순서와 equals의 결과가 일관되게 된다.
compareTo의 순서와 equals의 결과가 일관되지 않은 클래스도 동작은 한다.
하지만 이 클래스의 객체를 정렬된 컬렉션에 넣으면, 해당 컬렉션이 구현한
인터페이스(Collection, Set, Map)에 정의된 동작과 엇박자를 낼 것이다.
이 인터페이스들은 equals 메서드의 규약을 따른다고 되어 있지만
놀랍게도 정렬된 컬렉션들은 동치성을 비교할 때 equals 대신
compareTo를 사용하기 때문이다.
compareTo와 equals가 일관되지 않는 BigDecimal 클래스를 예로 생각해보자.
빈 HashSet 인스턴스를 생성한 다음 new BigDecimal("1.0")과
new BigDecimal("1.00")을 차례로 추가한다.
이 두 BigDecimal은 equals 메서드로 비교하면 서로 다르기 때문에
HashSet은 원소를 2개 갖게 된다.
하지만 HashSet 대신 TreeSet을 사용하면 원소를 하나만 갖게 된다.
compareTo 메서드로 비교하면 두 BigDecimal 인스턴스가 똑같기 때문이다.
public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);
return this.inflated().equals(xDec.inflated());
}public int compareTo(BigDecimal val) {
// Quick path for equal scale and non-inflated case.
if (scale == val.scale) {
long xs = intCompact;
long ys = val.intCompact;
if (xs != INFLATED && ys != INFLATED)
return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
}
int xsign = this.signum();
int ysign = val.signum();
if (xsign != ysign)
return (xsign > ysign) ? 1 : -1;
if (xsign == 0)
return 0;
int cmp = compareMagnitude(val);
return (xsign > 0) ? cmp : -cmp;
}■Comparable 인터페이스를 구현할 때의 주의사항
기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트(필드)를 추가했다면
compareTo 규약을 지킬 방법이 없다. (item 10)
(객체 지향적 추상화의 이점을 포기한다면 가능할수도 있다.)
이 주의사항은 equals에도 적용된다.
우회법은 역시 equals에서의 우회법과 같다.
Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면,
확장하는 대신 독립된 클래스를 만들고, 이 클래스에 원래 클래스의
인스턴스를 가리키는 필드를 두자.
그런 다음 내부 인스턴스를 반환하는 '뷰' 메서드를 제공하자.
이렇게 하면 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현해 넣을 수 있다.
클라이언트는 필요에 따라 바깥 클래스의 인스턴스를 필드 안에 담긴 원래 클래스의
인스턴스로 다룰 수도 있을 것이다.
Compareble은 타입을 인수로 받는 제네릭 인터페이스이므로
compareTo 메서드의 인수 타입은 컴파일타임에 정해진다.
입력 인수의 타입을 확인하거나 형변환할 필요가 없다는 뜻이다.
인수의 타입이 잘못됐다면 컴파일 자체가 되지 않는다.
또한 null을 인수로 넣어 호출하면 NullPointerException을 던져야 한다.
물론 실제로도 인수(이 경우 null)의 멤버에 접근하려는 순간 이 예외가 던져질 것이다.
■Comparable을 구현하지 않은 데이터를 비교할 때 :: Comparator 비교자
(위 규약대로 구현된) compareTo는 각 필드가 동치인지를
비교하는 게 아니라 그 순서를 비교한다.
객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출한다.
Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면
비교자(Comparator)를 대신 사용하면 된다.
비교자는 직접 만들거나 자바가 제공하는 것 중에 골라 쓰면 된다.
아래 코드는 item 10 에서 구현한 CaseInsensitiveString의 compareTo 메서드로,
자바가 제공하는 비교자를 사용하고 있다.
package effectivejava.chapter3.item14;
import java.util.*;
public final class CaseInsensitiveString
implements Comparable<CaseInsensitiveString> {
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
}CaseInsensitiveString이 Comparable<CaseInsensitiveString>을 구현한 것을 보자.
CaseInsensitiveString의 참조는 CaseInsensitiveString 참조와만 비교할 수 있다는
의미이다. 이는 Comparable을 구현할 때 일반적으로 따르는 패턴이다.
이 책의 2판에서는 compareTo 메서드에서 정수 기본 타입 필드를 비교할 때는
관계 연산자 <와 >를, 실수 기본 타입 필드를 비교할 때는 정적 메서드인
Double.compare와 Float.compare를 사용하라고 권했다.
그런데 java 7 부터는 상황이 변했다.
박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare를 이용하면
된다. compareTo 메서드에서 관계연산자 <와 >를 사용하는 방식은 오류를 유발할
수 있으니이제는 추천하지 않는다.
public final class Integer extends Number
implements Comparable<Integer>, Constable, ConstantDesc {
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
}■필드가 여러개인 클래스에서 Comparable 인터페이스 구현하기 :: 비교 순서
클래스에 핵심 필드가 여러 개라면 어느 것을 먼저 비교하느냐가 중요해진다.
가장 핵심적인 필드부터 비교하자. 가장 핵심이 되는 필드가 똑같다면
똑같지 않은 필드를 찾을 때까지 그 다음으로 중요한 필드를 비교하자.
아래는 item 10의 PhoneNumber 클래스의 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;
}java 8에서는 Comparator 인터페이스가 비교자 생성 메서드와 함께
연쇄 방식으로 비교자를 생성할 수 있게 되었다.
그리고 이 비교자들을 Comparable 인터페이스가 원하는
compareTo 메서드를 구현하는 데 활용할 수 있다.
(하지만 약간의 성능이 느려질 수 있다고 한다.)
아래는 PhoneNumber 클래스의 compareTo 메서드에 연쇄 방식을 적용한 코드다.
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는 객체 참조를 int 타입 키에 매핑하는 키 추출 함수를
인수로 받아, 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드다.
앞의 예에서 comparingInt는 람다를 인수로 받으며, 이 람다는 PhoneNumber에서
추출한 지역 코드를 기준으로 전화번호의 순서를 정하는 Comparator<PhoneNumber>를
반환한다. 이 람다에서 입력 인수의 타입(PhoneNumber pn)을 명시한 점에 주목하자.
자바의 타입 추론 능력이 이 상황에서도 타입을 알아낼 만큼 강력하지 않기 때문에
프로그램이 컴파일되도록 도와준 것이다.
두 전화번호의 지역 코드가 같을 수 있으니, 비교 방식을 더 다듬어야 한다.
여기서 두 번째 비교자 생성 메서드인 thenComparingInt가 등장한다.
thenComparingInt는 Comparator의 인스턴스 메서드로,
int 키 추출자 함수를 입력받아 다시 비교자를 반환한다.
(이 비교자는 첫 번째 비교자를 적용한 다음 새로 추출한 키로 추가 비교를 수행한다.)
thenComparingInt는 원하는 만큼 연달아 호출할 수 있다.
위 코드에서는 2개를 연달아 호출했으며 그중 첫 번째의 key로는 prefix를,
두 번째의 키로는 lineNum을 사용했다.
이번엔 thenComparingInt를 호출할 때 타입을 명시하지 않았다.
왜냐하면 자바의 타입 추론 능력이 이 정도는 추론해낼 수 있기 때문이다.
■Comparator의 보조 생성 메서드
Comparator는 수많은 보조 생성 메서드들로 중무장하고 있다.
long과 double용으로는 thenComparingLong과 thenComparingDouble이 있다.
short처럼 더 작은 정수 타입에는 위 PhoneNumber처럼 int용 메서드를 사용하면 된다.
마찬가지로 float은 double용 메서드를 사용하면 된다.
Comparator 이렇게 자바의 모든 숫자용 기본 타입에 대한 비교자 생성 메서드를 지원한다.
또한 객체 참조용 비교자 생성 메서드도 준비되어 있다.
우선 comparing 이라는 정적 메서드 2개가 다중정의되어 있다.
첫 번째는 키 추출자를 받아서 그 키의 자연적 순서를 사용한다.
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}두 번째는 키 추출자 하나와 추출된 키를 비교할 비교자까지 총 2개의 인수를 받는다.
public static <T, U> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator) {
Objects.requireNonNull(keyExtractor);
Objects.requireNonNull(keyComparator);
return (Comparator<T> & Serializable)
(c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
keyExtractor.apply(c2));
}또한 thenComparing이란 인스턴스 메서드가 3개 다중정의되어 있다.
첫 번째는 비교자 하나만 인수로 받아 그 비교자로 부차 순서를 정한다.
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);
};
}두 번째는 키 추출자를 인수로 받아 그 키의 자연적 순서로 보조 순서를 정한다.
default <U> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator)
{
return thenComparing(comparing(keyExtractor, keyComparator));
}마지막 세 번째는 키 추출자와 추출된 키를 비교할 비교자까지 총 2개의 인수를 받는다.
default <U extends Comparable<? super U>> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor)
{
return thenComparing(comparing(keyExtractor));
}■안티 패턴 :: 값의 차를 기준으로 비교
값의 차를 기준으로 첫 번째 값이 두 번째 값보다 작으면 음수를,
두 값이 같으면 0을, 첫 번째 값이 크면 양수를 반환하는 compareTo나
compare 메서드를 만들면 안된다.
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
}이 방식은 정수 오버플로를 일으키거나
IEEE 754 부동소수점 계산 방식에 따른 오류를 낼 수 있다.
또한 이번 아이템에서 설명한 방법대로 구현한 코드보다 월등히 빠르지도 않을 것이다.
아래의 두 가지 방법 중 하나를 선택하여 구현해야 한다.
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
}static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());
'개발서적읽기 > Effective Java - temp' 카테고리의 다른 글
[이펙티브 자바] item 32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) 2020.07.27 [이펙티브 자바] item 26 - 로 타입은 사용하지 말라 (0) 2020.07.19 [이펙티브 자바] item 71 - 필요 없는 검사 예외 사용은 피하라 (0) 2020.05.21 [이펙티브 자바] item 70 - 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라 (0) 2020.05.20 [이펙티브 자바] item 61 - 박싱된 기본 타입보다는 기본 타입을 사용하라 (0) 2020.05.15 댓글