728x90
Comparable 인터페이스란?
compareTo는Object의 메서드가 아니지만, 동작의 성격은 두 가지만 빼면Object의equals와 유사하다.compareTo는 단순 동치성 비교를 넘어 순서까지 비교할 수 있으며, 제네릭 메서드이다.Comparable을 구현했다는 것은 해당 클래스의 인스턴스들이 자연적인 순서(natural order)를 가지고 있음을 의미한다.Comparable을 구현한 객체들은 손쉽게 정렬할 수 있다.
예제 코드
public class WordList {
public static void main(String[] args){
Set<String> s = new TreeSet<>();
s.add("c");
s.add("b");
s.add("a");
Collections.addAll(s, args);
System.out.println(s);
}
}
// 실행 결과: [a, b, c]
CompareTo 메서드의 일반 규약
1. 대칭성
- 두 객체 참조의 순서를 바꿔 비교해도 결과가 일관적이어야 한다.
- 규약:
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
예외는y.compareTo(x)가 예외를 던질 때에만 발생한다.
2. 추이성
- 첫 번째 객체가 두 번째보다 크고, 두 번째가 세 번째보다 크다면, 첫 번째는 세 번째보다 커야 한다.
- 규약:
(x.compareTo(y) > 0 && y.compareTo(z) > 0) → x.compareTo(z) > 0
3. 반사성
- 크기가 같은 객체들끼리는 다른 객체와 비교해도 결과가 일관적이어야 한다.
- 규약:
x.compareTo(y) == 0 → sgn(x.compareTo(z)) == sgn(y.compareTo(z))
4. equals와의 일관성
compareTo로 수행한 동치성 테스트 결과는equals와 동일해야 한다.- 규약:
(x.compareTo(y) == 0) == (x.equals(y))
이 규약을 따르지 않으면 문서화해야 한다.
예: BigDecimal의 equals와 compareTo 불일치
public class TestBigDecimal {
public static void main(String[] args) {
final BigDecimal bigDecimal1 = new BigDecimal("1.0");
final BigDecimal bigDecimal2 = new BigDecimal("1.00");
final HashSet<BigDecimal> hashSet = new HashSet<>();
hashSet.add(bigDecimal1);
hashSet.add(bigDecimal2);
System.out.println(hashSet.size()); // 결과: 2
final TreeSet<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(bigDecimal1);
treeSet.add(bigDecimal2);
System.out.println(treeSet.size()); // 결과: 1
}
}
- 원인:
HashSet은equals를 기준으로 동치성을 확인하며,TreeSet은compareTo를 기준으로 동치성을 확인하기 때문이다.
CompareTo 메서드 작성 요령
1. 입력값 처리
Comparable은 제네릭 인터페이스이므로 입력값 검증이나 형변환이 필요 없다.null이 입력으로 들어오면NullPointerException을 던진다.
2. 핵심 필드 비교
- 가장 중요한 필드부터 비교하고, 결과가 0이 아니면 반환한다.
- 모든 핵심 필드를 비교해야 한다.
예: 기본 타입 필드가 여럿일 때의 비교
public int compareTo(final PhoneNumber phoneNumber) {
int result = Short.compare(areaCode, phoneNumber.areaCode);
if (result == 0) {
result = Short.compare(prefix, phoneNumber.prefix);
if (result == 0) {
result = Short.compare(lineNum, phoneNumber.lineNum);
}
}
return result;
}
3. Comparator 활용
Comparable을 구현하지 않은 필드나 비표준 순서를 사용해야 한다면Comparator를 이용한다.
관계 연산자 대신 Comparator 사용하기
1. 관계 연산자의 문제
- 관계 연산자(
<,>)는 정수 오버플로 등으로 인해 오류를 유발할 수 있다. Integer.compare,Double.compare등의 정적 메서드를 대신 사용한다.
예: 잘못된 비교자
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode(); // 정수 오버플로 위험
}
};
정적 compare 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};
Comparator 생성 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(Object::hashCode);
Comparator 인터페이스와 정적 메서드 활용
Comparator 정적 메서드
comparing: 객체 참조 키 추출comparingInt:int키 추출thenComparing: 추가 비교
Comparator는 자바에서 객체의 순서를 비교하는 데 사용되는 인터페이스이다. 특히, Comparator는 클래스가 Comparable을 구현하지 않아도 별도로 순서를 지정하거나, 특정 기준에 따라 객체를 정렬할 수 있는 유연성을 제공한다.
자바 8부터는 Comparator에 정적 메서드들이 추가되었으며, 이 메서드들은 객체 비교 로직을 간단하고 직관적으로 작성할 수 있게 해준다. 정적 메서드의 활용은 코드의 가독성을 높이고, 다단계 비교 로직을 보다 명확하게 표현할 수 있는 장점이 있다.
1. comparing
- 객체에서 특정 키(속성)를 추출해 비교자를 생성하는 데 사용된다.
- 일반적으로 람다 표현식과 함께 사용되어 비교 기준을 간단히 지정할 수 있다.
사용법
Comparator<T> comparing(Function<T, U> keyExtractor)
예제
Comparator<String> byLength = Comparator.comparing(String::length);
위 코드는 문자열의 길이를 기준으로 비교하는 Comparator를 생성한다.
2. comparingInt
- 객체에서 정수(int)형 키를 추출해 비교한다.
comparing과 유사하지만, 기본 타입 int를 다루기 때문에 박싱(Boxing) 및 언박싱(Unboxing)의 오버헤드를 줄인다.
사용법
Comparator<T> comparingInt(ToIntFunction<T> keyExtractor)
예제
Comparator<PhoneNumber> byAreaCode = Comparator.comparingInt(phone -> phone.areaCode);
위 코드는 PhoneNumber 객체의 areaCode 필드를 기준으로 정렬하는 Comparator를 생성한다.
3. thenComparing
- 첫 번째 비교에서 두 객체가 동등하다고 판단되면, 추가로 비교할 기준을 지정한다.
thenComparing은 다단계 비교를 쉽게 구현할 수 있도록 도와준다.
사용법
Comparator<T> thenComparing(Comparator<? super T> other)
예제
Comparator<PhoneNumber> byFullDetails =
Comparator.comparingInt(phone -> phone.areaCode)
.thenComparingInt(phone -> phone.prefix)
.thenComparingInt(phone -> phone.lineNum);
위 코드는 PhoneNumber 객체를 areaCode, prefix, lineNum 순서로 비교한다. 만약 areaCode가 같으면 prefix를, prefix까지 같으면 lineNum을 기준으로 비교한다.
정적 메서드 활용: 예제 상세 설명
예제 코드
private static final Comparator<PhoneNumber> COMPARATOR =
Comparator.comparingInt((PhoneNumber phoneNumber) -> phoneNumber.areaCode)
.thenComparingInt(phoneNumber -> phoneNumber.prefix)
.thenComparingInt(phoneNumber -> phoneNumber.lineNum);
public int compareTo(PhoneNumber phoneNumber) {
return COMPARATOR.compare(this, phoneNumber);
}
코드의 동작 원리
- 첫 번째 비교 기준:
areaCodeComparator.comparingInt((PhoneNumber phoneNumber) -> phoneNumber.areaCode)는areaCode필드를 기준으로 두 객체를 비교하는Comparator를 생성한다.
예:areaCode가 101인 객체는 102인 객체보다 작다. - 두 번째 비교 기준:
prefixthenComparingInt(phoneNumber -> phoneNumber.prefix)는 첫 번째 비교 결과가 0(동등)인 경우,prefix필드를 기준으로 추가 비교를 수행한다.
예: 두 객체의areaCode가 모두 101이라면,prefix를 비교한다. - 세 번째 비교 기준:
lineNumthenComparingInt(phoneNumber -> phoneNumber.lineNum)는 두 번째 비교 결과도 0일 경우, 마지막으로lineNum필드를 비교한다.
예: 두 객체의areaCode와prefix가 모두 같다면,lineNum을 기준으로 비교한다. compareTo메서드와의 연결compareTo메서드는COMPARATOR.compare(this, phoneNumber)를 호출하여 현재 객체(this)와 비교 대상(phoneNumber)의 순서를 반환한다.
이는Comparable인터페이스를 구현하는 간결한 방식이다.
장점
- 가독성
- 정적 메서드를 사용하면 비교 로직이 간단하고 직관적이다.
- 코드가 복잡해지지 않으며, 각 비교 기준을 명확히 구분할 수 있다.
- 유연성
- 다단계 비교를 쉽게 작성할 수 있다.
- 필요 시
Comparator를 동적으로 생성하여 다양한 비교 로직에 재사용할 수 있다.
- 성능 최적화
comparingInt와 같은 메서드는 기본 타입(int)을 직접 다루어 성능을 높인다.
추가 예제: 복잡한 객체 비교
문제: 학생 객체(Student) 정렬
- 필드:
grade(학년),classNumber(반),name(이름)
목표:
- 학년 순서로 정렬
- 학년이 같다면 반 순서로 정렬
- 반도 같다면 이름 순으로 정렬
코드
Comparator<Student> studentComparator =
Comparator.comparingInt(student -> student.grade)
.thenComparingInt(student -> student.classNumber)
.thenComparing(student -> student.name);
public int compareTo(Student other) {
return studentComparator.compare(this, other);
}
설명:
comparingInt(student -> student.grade): 학년(grade)을 기준으로 1차 정렬.thenComparingInt(student -> student.classNumber): 반(classNumber)을 기준으로 2차 정렬.thenComparing(student -> student.name): 이름(name)을 기준으로 3차 정렬.
정리
- 순서를 고려해야 하는 값 클래스를 작성할 때는 반드시
Comparable인터페이스를 구현해야 한다. compareTo메서드 작성 시<와>연산자를 지양하고, 정적 메서드나Comparator를 활용해야 한다.compareTo와equals는 일관성을 유지해야 한다.
728x90
'Programming Language > Java' 카테고리의 다른 글
| [이펙티브 자바]Item.16-public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2024.12.02 |
|---|---|
| [이펙티브 자바]Item.15-클래스와 멤버의 접근 권한을 최소화해라 (0) | 2024.11.26 |
| [이펙티브 자바]Item.13-clone 재정의는 주의해서 진행하라 (1) | 2024.11.20 |
| [이펙티브 자바]Item.12-toString을 항상 재정의하라 (0) | 2024.05.14 |
| [이펙티브 자바]Item.11-equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2024.05.14 |