🧐 Comparable
Comparable은 다음과 같은 인터페이스입니다.
public interface Comparable<T> {
public int compareTo(T o);
}
Comparable은 이를 구현한 객체에 자연적인 순서(natural order)가 있음을 의미하며, 그러한 순서에 따라 정렬등을 할 수 있도록 해줍니다.
Comparable은 수많은 API에서 활용되며, 단지 이를 구현함으로써 그러한 수많은 API의 기능들을 사용할 수 있게 됩니다.
대표적으로는 Collections.sort 메서드가 있습니다.
public static <T extends Comparable<? super T>> void sort(List<T> list) {
list.sort(null);
}
알파벳, 숫자와 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현해야 합니다.
🧐 compareTo 메서드의 일반 규약
compareTo 메서드를 보면 다음과 같이 설명되어 있습니다.
순서를 위해 이 객체를 주어진 객체와 비교합니다.
이 객체가 주어진 객체보다 작은 경우 음의 정수를 반환해야 하고, 같은 경우에는 0, 큰 경우 양의 정수를 반환합니다.
만약 해당 객체와 비교할 수 없는 타입이 주어진다면, ClassCastException을 던져야 합니다.
아래에서 sgn(표현식)은 표현식의 값이 음수인 경우 -1을, 0인 경우 0을, 양수일 때 1을 반환하도록 정의된 함수입니다.
📕 대칭성
Comparable을 구현한 클래스는
모든 x 및 y에 대해 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))을 보장해야 합니다.
(따라서 y.compareTo(x)가 예외를 throw하는 경우 x.compareTo(y)가 예외를 throw해야 합니다.)
📙 추이성
(x.compareTo(y) > 0 && y.compareTo (z) > 0)은 x.compareTo(z) > 0을 보장해야 합니다.
- x.compareTo(y)==0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))여야 합니다.
📒 반사성
크기가 같은 객체들끼리는, 다른 객체와 비교한 결과가 모두 동일해야 합니다.
x.compareTo(y)==0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))여야 합니다.
📗 equals()
아래는 필수는 아니지만 꼭 지키는 게 좋습니다.
(x.compareTo(y)==0) == (x.equals(y)) 를 만족하는 것이 좋습니다.
만약 이를 만족하지 않는다면, 다음과 같이 그 사실을 명시해야 합니다.
"Note: this class has a natural ordering that is inconsistent with equals"
🧐 기존 클래스를 확장한다면?
기존 클래스를 확장한 클래스에서 필드가 추가되어 해당 필드 또한 순서로 고려해야 한다면, compareTo의 규약을 지킬 수 없습니다.
이를 우회하는 방법은 컴포지션을 이용하는 것인데,
기존 클래스를 확장하는 대신 독립된 클래스를 만든 이후, 해당 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두는 것입니다.
예시는 다음과 같습니다.
class Parent implements Comparable<Parent> {
private int a;
@Override
public int compareTo(final Parent o) {
return a - o.a;
}
}
class Child implements Comparable<Child> {
private Parent parent;
protected int b;
@Override
public int compareTo(final Child o) {
if (b - o.b == 0) {
return parent.compareTo(o.parent);
}
return b - o.b;
}
}
🧐 compareTo와 equals가 일치하지 않는다면?
compareTo의 마지막 규약을 어긴다면, 해당 클래스를 정렬된 컬렉션에 넣을 때 문제가 생길 수 잇습니다.
Collection, Set, Map 등은 equals 메서드의 규약을 따른다고 되어 있지만, 정렬된 컬렉션들은 동치성을 비교할 때 equlas 대신 compareTo를 사용하기 때문입니다.
마지막 규약을 어긴 대표적인 클래스로는 BigDecimal이 있습니다.
final BigDecimal bigDecimal1 = new BigDecimal("1.0");
final BigDecimal bigDecimal2 = new BigDecimal("1.00");
final Set<BigDecimal> hashSet = new HashSet<>();
hashSet.add(bigDecimal1);
hashSet.add(bigDecimal2);
System.out.println(hashSet.size()); // 2
final Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(bigDecimal1);
treeSet.add(bigDecimal2);
System.out.println(treeSet.size()); // 1
TreeSet은 정렬된 컬렉션으로, 내부에서 동치성 비교를 할 때 equals 대신 compareTo를 사용하기 때문에 위와 같은 현상이 발생합니다.
🧐 compareTo 작성 요령
- Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로, compareTo의 인수타입은 컴파일 시에 정해집니다. 즉, 입력 인수 확인이나 형변환을 할 필요가 없습니다.
- null을 인수로 넣으면 NullPointerException을 던져야 합니다.
- compareTo는 동치가 아닌 순서를 비교합니다.
- 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출합니다.
- Comparable을 구현하지 않은 필드나, 표준이 아닌 순서로 비교해야 한다면 Comparator을 대신 사용한다.
🧐 기본 타입 필드 비교하기
기본 타입의 필드를 비교하는 경우, compareTo 메서드에서 관계 연산자 >, < 를 사용하는 대신, 박싱된 기본 타입 클래스들에 존재하는 정적 메서드 compare를 이용하는 것을 추천합니다.
class MyObject implements Comparable<MyObject> {
private int a;
private double b;
@Override
public int compareTo(final MyObject o) {
if (a == o.a) {
return Double.compare(b, o.b);
}
return Integer.compare(a, o.a);
}
}
🧐 핵심 필드가 여러개인 경우
당연하게도, 어느 필드를 먼저 비교하는지가 중요해집니다.
이러한 경우 가장 핵심적인 필드부터 비교해야 하며, 순서가 결정된다면 이후 필드는 고려하지 않고 거기서 끝마칩니다.
class MyObject implements Comparable<MyObject> {
private int a;
private double b;
@Override
public int compareTo(final MyObject o) {
if (a == o.a) {
return Double.compare(b, o.b);
}
return Integer.compare(a, o.a);
}
}
자바 8에서는 Comparator 인터페이스에 일련의 비교자 생성 메서드가 추가되어, 메서드 연쇄 방식으로 Comparator를 생성할 수 있게 되었습니다.
static class MyObject implements Comparable<MyObject> {
private int a;
private double b;
private static final Comparator<MyObject> COMPARATOR =
Comparator.comparingInt((MyObject myObj) -> myObj.a)
.thenComparing(myObj -> myObj.b);
@Override
public int compareTo(final MyObject o) {
return COMPARATOR.compare(this, o);
}
}
🧐 값의 차이를 반환하는 compareTo는 사용하지 말자
다음과 같이 값의 차를 통해 compareTo를 구현하는 것을 종종 볼 수 있습니다.
class MyInt implements Comparable<MyInt> {
private int a;
@Override
public int compareTo(final MyInt o) {
return a - o.a;
}
}
그러나 이 방식은 정수 오버플로우를 일으키거나, 소수인 경우 부동소수점 계산 방식에 따른 오류를 발생시킬 수 있기 때문에 사용하는 것은 좋지 않습니다.
static class MyInt implements Comparable<MyInt> {
private int a;
public MyInt(final int a) {
this.a = a;
}
@Override
public int compareTo(final MyInt o) {
return a - o.a;
}
}
static class CollectMyInt implements Comparable<CollectMyInt> {
private int a;
public CollectMyInt(final int a) {
this.a = a;
}
@Override
public int compareTo(final CollectMyInt o) {
return Integer.compare(a, o.a);
}
}
void test() {
// given
MyInt bigMyInt = new MyInt(10);
MyInt smallMyInt = new MyInt(Integer.MIN_VALUE);
System.out.println(bigMyInt.compareTo(smallMyInt)); // 오류 -> 음수 반환
System.out.println(smallMyInt.compareTo(bigMyInt)); // 오류 -> 양수 반환
CollectMyInt bigCollectMyInt = new CollectMyInt(10);
CollectMyInt smallCollectMyInt = new CollectMyInt(Integer.MIN_VALUE);
System.out.println(bigCollectMyInt.compareTo(smallCollectMyInt));
System.out.println(smallCollectMyInt.compareTo(bigCollectMyInt));
}
'☕️ Java > 이펙티브 자바' 카테고리의 다른 글
[Effective Java] 아이템 33 - 타입 안전 이종 컨테이너를 고려하라 (+ 슈퍼 타입 토큰) (0) | 2023.03.06 |
---|---|
[Effective Java] 아이템 31 - 한정적 와일드카드를 이용해 API의 유연성을 높이라 (+ 변성) (3) | 2023.02.26 |
[Effective Java] 아이템 17 - 변경 가능성을 최소화하라 (0) | 2023.02.23 |
[Effective Java] 아이템 15 - 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2022.02.11 |
[Effective Java] 아이템 10 - equals는 일반 규약을 지켜 재정의하라 (0) | 2022.02.05 |