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에 대해
- 추이성:
(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의 메서드 체인을 활용.
'Programming Language > Java' 카테고리의 다른 글
| 비한정적 와일드 카드 & 한정적 와일드 카드 (0) | 2025.03.17 |
|---|---|
| 공변성 & 반공변성 & 불공변성? (0) | 2025.03.17 |
| Reflection (0) | 2025.03.08 |
| [Collection] Enum? (0) | 2025.03.06 |
| [Collection] Map 인터페이스와 주요 구현체 학습 (0) | 2025.03.06 |
