14. Comparable을 구현할지 고려하라
1. Comparable 인터페이스란?
정의:
자바에서 객체 간의 순서를 정의하고 비교하는 데 사용되는 인터페이스
public interface Comparable<T> { int compareTo(T o); }
compareTo(T o)
메서드:Comparable
인터페이스의 유일한 메서드.객체의 순서를 비교하여 정렬과 같은 작업에 활용
Object
의equals
와 유사하지만, 순서 비교 및 제네릭 타입 지원에서 차이.제네릭(
Comparable<T>
)을 사용하여 타입 안정성 보장.
2. Comparable 구현의 이점
자연스러운 순서: 클래스의 인스턴스에 자연스러운 순서 부여.
쉬운 정렬:
Arrays.sort(a);
와 같이 간단하게 배열 정렬 가능.다양한 활용: 검색, 극단값 계산, 자동 정렬 컬렉션(TreeSet, TreeMap) 등에서 활용.
제네릭 알고리즘 및 컬렉션과의 호환: 수많은 제네릭 알고리즘과 컬렉션을 활용 가능.
Arrays.sort(): 배열을 정렬하는 제네릭 알고리즘
Collections.sort(): 리스트를 정렬하는 제네릭 알고리즘
TreeSet: 정렬된 집합을 유지하는 컬렉션
TreeMap: 정렬된 키-값 쌍을 유지하는 컬렉션
이러한 알고리즘과 컬렉션들은 Comparable를 구현한 객체들을 자동으로 정렬하거나 순서를 유지하도록 사용.
Java 플랫폼 라이브러리 활용:
대부분의 값 클래스와 열거 타입이
Comparable
구현.Integer, String, Date, Enum 등
3. compareTo 메서드 일반 규약 (equals 규약과 유사)
주어진 객체보다 작으면 음의 정수, 같으면 0, 크면 양의 정수를 반환한다.
비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다
반사성:
모든 x에 대해
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
x.compareTo(y)
가 예외를 던지면,y.compareTo(x)
도 예외를 던져야 함.
추이성:
(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) == (x.equals(y))
꼭 지켜야 하는 것은 아니지만, 지키지 않을 경우 "주의: 이 클래스의 순서는 equals 메서드와 일관되지 않다"라고 명시해야 함.
참고:
sgn(표현식)
: 표현식의 값에 따라 -1, 0, 1을 반환하는 부호 함수.compareTo
는 타입이 다른 객체에 대해ClassCastException
을 던져도 됨 (대부분 그렇게 함).다른 타입 간 비교는 객체들이 구현한 공통 인터페이스를 통해 이루어질 수 있음.
4. compareTo 규약 미준수 시 문제점
hashCode
규약 미준수 시 해시 기반 컬렉션(HashMap, HashSet)과 문제 발생.compareTo
규약 미준수 시 비교 기반 컬렉션/클래스(TreeSet, TreeMap, Collections, Arrays)와 문제 발생.
5. 기존 클래스 확장 시 compareTo 규약 문제
public class Point implements Comparable<Point> {
protected int x;
public Point(int x) { this.x = x; }
@Override
public int compareTo(Point other) {
int result = Integer.compare(this.x, other.x);
return result;
}
}
public class ColorPoint extends Point implements Comparable<ColorPoint> {
private String color;
public ColorPoint(int x, String color) {
super(x);
this.color = color;
}
@Override
public int compareTo(ColorPoint other) {
int result = super.compareTo(other);
if (result == 0) result = this.color.compareTo(other.color);
return result;
}
}
문제: 기존 클래스를 확장하여 새로운 값 컴포넌트를 추가하면
compareTo
규약을 지키기 어려움.Point
클래스를 상속받아ColorPoint
클래스를 만들고color
필드를 추가할 경우,Point
와ColorPoint
간의 비교가 모호해짐
해결책: 상속 대신 컴포지션 사용.
독립된 클래스를 만들고, 원래 클래스의 인스턴스를 가리키는 필드를 추가.
내부 인스턴스를 반환하는 '뷰' 메서드 제공.
결과: 새로운 compareTo 작성이 가능, 클라이언트는 원래 클래스처럼 사용 가능
public class ColorPoint implements Comparable<ColorPoint> {
private Point point;
private String color;
public ColorPoint(Point point, String color) {
this.point = point;
this.color = color;
}
@Override
public int compareTo(ColorPoint other) {
int result = this.point.compareTo(other.point);
if(result == 0) result = this.color.compareTo(other.color);
return result;
}
}
6. compareTo와 equals의 일관성 (권장)
권장:
compareTo
의 결과와equals
의 결과가 같도록 구현하는 것이 좋음.일관성 유지 시:
compareTo
로 정렬된 순서와equals
의 결과가 일관됨.일관성 미유지 시: 정렬된 컬렉션(TreeSet, TreeMap)에서 예상치 못한 동작 발생 가능성.
정렬된 컬렉션은
equals
대신compareTo
를 사용하기 때문.BigDecimal
compareTo는 수치적인 값만을 비교하지만, equals는 수치적인 값과 scale(소수점 자리수)까지 비교합니다.
예를 들어, BigDecimal("5.0")과 BigDecimal("5.00")은 compareTo로는 같지만, equals로는 다릅니다.
BigDecimal bd1 = new BigDecimal("5.0"); BigDecimal bd2 = new BigDecimal("5.00"); int compareResult = bd1.compareTo(bd2); System.out.println("compareTo 결과: " + compareResult); // 0 (같음) boolean equalsResult = bd1.equals(bd2); System.out.println("equals 결과: " + equalsResult); // false (다름)
hashSet vs treeSet
Set<BigDecimal> hashSet = new HashSet<>(); hashSet.add(bd1); hashSet.add(bd2); System.out.println("HashSet 크기: " + hashSet.size()); // 2 (다르게 처리) Set<BigDecimal> treeSet = new TreeSet<>(); treeSet.add(bd1); treeSet.add(bd2); System.out.println("TreeSet 크기: " + treeSet.size()); // 1 (같게 처리)
7. compareTo 메서드 작성 요령
타입 안정성:
Comparable
은 제네릭 인터페이스이므로 인수 타입은 컴파일 타임에 결정.타입 확인, 형변환 불필요.
잘못된 타입은 컴파일 오류 발생.
null
입력 시NullPointerException
발생.
필드 비교:
객체 참조 필드:
compareTo
재귀 호출.Comparable
미구현 필드, 표준이 아닌 순서:Comparator
사용 (직접 만들거나, 자바 제공Comparator
사용).
핵심 필드 우선 비교: 여러 핵심 필드가 있다면 가장 핵심적인 필드부터 비교.
비교 결과가 0이 아니면 즉시 반환, 0이면 다음 필드 비교.
Java 8 이후 비교자 생성 메서드 활용:
import static java.util.Comparator.*; // 정적 임포트 Comparator<Person> ageThenName = comparingInt(Person::getAge) .thenComparing(Person::getName) .thenComparing(Person::getAddress); java.util.Arrays.sort(people, ageThenName);
Comparator
인터페이스의 비교자 생성 메서드(comparingInt, thenComparingInt 등)를 이용해 메서드 연쇄 방식으로 비교자 생성.정적 임포트와 함께 사용하면 코드가 간결해짐, 남용시 약간의 성능 저하 가능성 있음.
comparingInt
: 객체에서 int 키를 추출하여 비교thenComparingInt
: 이전 비교 결과가 같을 때, 추가로 int 키를 추출하여 비교 (연쇄 호출 가능)
다양한 보조 생성 메서드:
comparingLong
,comparingDouble
,comparing
,thenComparing
등.
값의 차를 이용한 비교 지양:
// Bad public int compareTo(Person other) { return this.age - other.age; } // Good return Integer.compare(this.age, other.age); // or return Comparator.comparingInt(o -> o.getAge()); return Comparator.comparingInt(Person::getAge);
정수 오버플로, 부동소수점 오류 발생 가능성.
정적
compare
메서드나Comparator
의 비교자 생성 메서드 사용 권장.관계연산자
<
,>
사용 지양
핵심 정리
Comparable 인터페이스의 정의와 목적:
객체 간의 순서를 비교하기 위해 제네릭 타입을 활용하여 타입 안전성을 보장
compareTo 메서드가 순서 비교의 기준
구현의 이점:
자연스러운 정렬, 간편한 배열 정렬, 다양한 컬렉션과 제네릭 알고리즘에서의 활용이 가능
자바 라이브러리와의 호환성이 뛰어남.
compareTo 메서드의 규약:
반사성, 추이성, 대칭성, 그리고 (권고되는) equals와의 일관성을 유지하는 것이 중요함.
규약을 어길 경우 해시 기반 및 정렬 기반 컬렉션에서 문제 발생 가능성
상속과 비교 규약 문제:
기존 클래스를 확장할 때 새로운 값 컴포넌트를 추가하면 compareTo 규약을 지키기 어려워질 수 있으므로, 컴포지션을 통해 해결하는 방안을 제시함.
작성 요령 및 주의사항:
제네릭을 통해 타입 검증을 수행하고, 잘못된 타입은 컴파일 오류를 유도.
필드 비교 시 핵심 필드를 우선으로 하고, 재귀적 비교 혹은 Comparator 사용을 권장.
숫자 차이 연산 대신 Integer.compare와 같은 안전한 비교 방법 또는 Comparator의 메서드 체인을 활용.
Last updated