티스토리 뷰
* 이펙티브 자바 2/E를 읽고 공부하기 위해 기록한 게시글입니다.
12. Comparable 구현을 고려하라
두 객체를 비교하는데 사용되는 compareTo 메서드는 Object에 선언되어 있지 않다. Comparable 인터페이스에 선언되어있는 유일한 메서드이다.
- Object의 equals 메서드와 특성은 비슷하지만, 단순 동치성 비교(== 비교, 동등성)에 더해 순서 비교가 가능하며 조금 더 일반적인 형태(제네릭 하다).
public interface Comparable<T> {
public int compareTo(T o);
}
Comparable 인터페이스를 구현하는 클래스의 객체들은 자연적 순서(Natural Ordering)을 갖게 된다. 덕분에 검색하거나 최대/최소값을 계산하기도 간단하며, 컬렉션을 정렬된 상태로 유지하는 것도 쉽다. Comparable을 구현한 객체들의 배열을 정렬하는 것이 매우 간단해진다.
(예) Arrays.sort(arr), Collections.sort(list)
Comparable을 구현한 클래스는 다양한 제네릭 알고리즘 및 Comparable 인터페이스를 이용하도록 작성된 컬렉션 구현체와도 전부 연동해서 사용할 수 있다.
- 자바 라이브러리에 포함된 거의 모든 값 클래스는 Comparable 인터페이스를 구현하고 있다.
public final class Integer extends Number implements Comparable<Integer> {
...
}
- 알파벳 순서나 값의 크기, 시간적 선후관계와 같이 명확한 자연적 순서를 따르는 값 클래스를 구현할 때에는 Comparable 인터페이스를 구현할 것을 꼭!! 고려해 봐야 한다.
compareTo 메서드의 일반 규약은 equals 메서드와 비슷하다.
- 이 객체와 인자로 주어진 객체를 비교한다. 이 객체의 값이 인자로 주어진 객체보다 작으면 음수, 같으면 0, 크면 양수를 반환한다. (인자로 전달된 객체의 자료형이 이 객체와 비교 불가능한 자료형인 경우는 예외를 던진다.)
1. 객체 참조를 비교하는 방향을 뒤집어도 객체 간 대소관계는 그대로 유지되어야 한다.
- compareTo를 구현할 때에는 모든 x, y에 대해 x.compareTo(y) == -y.compareTo(x)가 성립해야 한다. 그 역도 마찬가지.
2. 추이성(transitivity)가 만족되어야 한다.
- x.compareTo(y) > 0 && y.compareTo(z) > 0 이면 x.compareTo(z) > 0 이다
3. 비교 결과 같다고 판정된 모든 객체 각가을 다른 어떤 객체와 비교할 겅우 그 결과는 모두 동일해야 한다.
- x.compareTo(y) == 0이면 (x == y 이면) x.compareTo(z) == y.compareTo(z) 관계가 모든 z에 대해 성립해야 한다.
4. 권장사항 (compareTo 메서드가 가정하는 순서관계는 equals에 부합한다)
- (x.compareTo(y) == 0) == (x.equals(y)) 를 만족하는 것을 강력히 권장한다.
===
4번 항목에서 compareTo 메서드가 가정하는 순서관계가 equals에 부합하지 않는 경우는 어떤 것이 있을까?
-> BigDecimal 클래스를 예로 들 수 있다.
new BigDecimal("1.0")과 new BigDecmial("1.00") 객체를 만든다고 했을 때
HashSet에 두 객체를 추가하면 두 개의 객체가 전부 삽입되지만(equals로는 서로 다르다), TreeSet에 두 객체를 추가하면 하나의 객체밖에 삽입되지 않는다. compareTo로 비교했을 때 두 객체는 같은 객체이기 때문이다.
-> BigDecimal 객체는 equals 사용 시 소숫점 끝의 0까지 동일해야 true를 반환하고, compareTo 사용 시에는 소숫점 끝의 0은 무시하고 동일하면 true를 반환하는 차이점이 존재하기 때문.
* BigDecimal 클래스는 자바에서 숫자를 소숫점까지 가장 정밀하게 저장하고 표현할 수 있는 유일한 방법이다
- double 역시 소숫점의 정밀도가 완벽하지 않아 오차가 발생할 수 있다. 돈과 같이 매우 예민한 소숫점을 다룬다면 BigDecimal을 사용하는 것이 필수.
===
compareTo 메서드는 equals 메서드와는 달리 서로 다른 클래스 객체에는 적용될 필요가 없다. 한 클래스 객체 사이에 자연적 순서가 존재하기만 하면 어떤 것이든 자연스럽게 이 규약을 만족하게 된다.
- compareTo도 equals와 마찬가지로 반사성, 대칭성, 추이성을 만족해야 한다는 것.
- compareTo는 equals와 달리 컴파일 시간에 인자 자료형이 정적으로 결정된다. 그렇기에 인자로 받은 객체의 자료형을 검사하거나 따로 형변환을 거칠 필요가 없다.
compareTo 메서드의 필드 비교방식은 동등성 검사(같은지 / 다른지) 라기 보다는 순서 비교 (-1, 0, 1)이다.
객체 참조필드는 compareTo 메서드를 재귀적으로 호출하여 비교한다. 비교할 필드가 compareTo 메서드를 구현하지 않고 있거나 특이한 순서관계를 가지는 경우에는 Comparator를 명시적으로 사용할 수도 있다.
hashCode 규약을 따르지 않는 클래스는 해시를 써서 구현한 클래스를 오동작 시킬 수 있다. compareTo 규약 역시 compareTo 규약을 따르지 않는 클래스는 비교 연산에 기반한 클래스들을 오동작 시킬 수 있다.
- TreeSet, TreeMap과 같은 정렬된 컬렉션, Arrays와 Collections같은 유틸리티 클래스 등..
클래스에 선언된 중요 필드가 여러개인 경우에는 필드 비교 순서가 중요하다.
- 가장 중요한 필드부터 시작해 차례로 비교해야 한다.
- 비교 결과가 0(동치) 아닌 값이면 비교를 중단하고 결과를 리턴. 0인 경우에는 다음 중요한 필드를 통해 비교작업을 계속 진행.
* compareTo에서 순서를 비교할 때에는 관계연산자(>, <)를 이용하기 보다는 Wrapper클래스에 정의된 정적 메서드인 compare 메서드를 이용하면 편리하다.
* 특히 기본 자료형 필드를 비교할 때 a - b 와 같이 비교하는 것은 좋지 않다!! (오버플로우 발생 가능)
@Override
public int compareTo(User o) {
return Integer.compare(o.idx, idx);
// return o.idx - idx;
}
///
// Integer.class
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
// Double.class
public static int compare(double d1, double d2) {
if (d1 < d2)
return -1; // Neither val is NaN, thisVal is smaller
if (d1 > d2)
return 1; // Neither val is NaN, thisVal is larger
// Cannot use doubleToRawLongBits because of possibility of NaNs.
long thisBits = Double.doubleToLongBits(d1);
long anotherBits = Double.doubleToLongBits(d2);
return (thisBits == anotherBits ? 0 : // Values are equal
(thisBits < anotherBits ? -1 : // (-0.0, 0.0) or (!NaN, NaN)
1)); // (0.0, -0.0) or (NaN, !NaN)
}
(+) Comparable 인터페이스와 Comparator의 차이가 뭘까?
Comparable 인터페이스는 앞에서 배운것과 같이 자연적 순서를 따르는 경우 compareTo 메서드를 이용해 비교했다.
반면 Comparator 인터페이스의 경우는 기본 정렬 기준과 다르게 정렬하고 싶을 때 주로 사용되어진다. (내림차순 정렬 등...) 대개 익명 객체로 만들어져 사용되어짐.
Comparator 인터페이스의 경우 내부에 상당히 많은 메서드들이 선언 및 구현되어있는데, 사용자가 재정의 해야 할 것은 compare(T o1, T o2) 메서드 하나 뿐이다.
* 자바 8부터 인터페이스에서도 메서드를 미리 구현 해놓을 수 있게 되었다. default 혹은 static 메서드가 그 대상인데, default 메서드는 구현 클래스에서 재정의가 가능하고, static 메서드는 재정의가 불가능하다는 차이점이 있다.
default 혹은 static 메서드는 구현 클래스에서 반드시 재정의 할 필요가 없음. 이외의 메서드들은 이전과 마찬가지로 추상 메서드로서 구현 클래스에서 반드시 재정의 하여 사용해야 한다.
public interface Comparator<T> {
// 추상 메서드 (반드시 재정의)
int compare(T o1, T o2);
...
// 선택적 재정의
default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}
...
// 재정의 불가
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}
...
}
Comparable이나 Comparator나 둘 다 객체를 비교할 수 있도록 하는 것은 동일하다.
단 차이가 있다면 Comparable은 자기 자신과 인자로 받은 객체 둘을 비교하는 것이고 [ compareTo(T o) ]
Comparator은 자기 자신과는 무관하게 인자로 받은 두 객체를 비교하는 것이다 [ compare(T o1, T o2) ]
-> 자기 자신과 무관하게 인자로 받은 두 객체를 비교하는데 사용되기 때문에 주로 익명 객체를 만들어서 사용.
--> 익명 객체를 사용하기 때문에 비교 기준을 여러 종류 만들어서 사용할 수 있음.
<-> Comparable의 경우 compareTo 메서드에 정의된 비교기준 하나만 사용할 수 있는 것에 대비되는 부분.
'JAVA' 카테고리의 다른 글
getter/setter 메서드를 활용하자 (0) | 2022.07.23 |
---|---|
클래스와 멤버의 접근 권한을 최소화하자 (0) | 2022.07.22 |
객체 복제가 필요하다면 복사 생성자/팩토리 (0) | 2022.07.21 |
toString은 재정의해서 사용하자 (0) | 2022.07.21 |
equals를 재정의할 때는 hashCode도 같이 재정의하자 (0) | 2022.07.18 |