Comparable
- 특정 클래스의 인스턴스들 간에 순서를 정할 수 있는 기준을 제공합니다.
compareTo
라는 유일한 메서드를 구현해야 합니다.- 자연스러운 순서(natural order)가 있는 값들(예: 숫자, 문자열, 날짜 등)을 처리하기에 적합합니다.
- 단순 동치성 비교에 더해 순서까지 비교할 수 있으며 제네릭하다
public interface Comparable<T> {
int compareTo(T t);
}
compareTo
ClassCastException : 특정 클래스의 객체를 호환되지 않는 다른 클래스의 객체로 변환하려고 할 때 발생하는 런타임 에러
compareTo 반환값의 기준
- 기준 객체 < 주어진 객체 → -1 반환 "A".compareTo("B") = -1
- 기준 객체 == 주어진 객체 → 0 반환
- 기준 객체 > 주어진 객체 → 1 반환
class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) { // 정렬 기준을 나타내는 정수 값을 반환
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + " (" + age + "세)";
}
}
public class Main {
public static void main(String[] args) {
Person[] people = {
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
};
System.out.println(Arrays.toString(people));
// [Alice (30세), Bob (25세), Charlie (35세)]
Arrays.sort(people);
System.out.println(Arrays.toString(people));
// [Bob (25세), Alice (30세), Charlie (35세)]
}
}
- Comparable를 구현했다는 것은 해당 클래스의 인스턴스들에는 순서가 있음을 뜻한다.
→ Comparable을 구현하면 손쉽게 컬렉션을 정렬할 수 있다.
→ Comparable을 구현한 객체들의 배열이 있다면 Arrays.sort() 를 통해 손쉽게 정렬할 수 있다.
public class Main {
public static void main(String[] args) {
String[] arr = {"B", "C", "D", "A", "A"};
Set<String> s = new TreeSet<>();
Collections.addAll(s, arr);
System.out.println(s); // ["A", "B", "C", "D"]
}
}
- 검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 역시 쉽게 할 수 있다.
- 다음 프로그램은 arr의 값들을 TreeSet에 넣어 (중복을 제거하고) 알파벳순으로 출력한다
- → String에는 Comparable이 구현되어있기 때문이다
compareTo 메서드 일반 규약
sgn 은 표현식이며 부호 함수를 의미한다
표현식이 음수, 0, 양수일 때 -1, 0, 1을 반환한다
반사성
모든 x, y에 대하여 sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) 가 성립한다.
즉, 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다.
추이성 (삼단논법)
x.compareTo(y) > 0 && y.compareTo(z) > 0 가 성립하면
x.compareTo(z) > 0 도 성립한다.
(x > y, y > z) 이면 (x > z)
대칭성
모든 z에 대해서 x.compareTo(y) == 0 이면sgn(x.compareTo(z)) == sgn(y.compareTo(z)) 이 성립
즉, 크기가 같은 객체끼리는 어떤 객체와 비교하더라도 항상 결과가 같아야 한다
필수는 아니지만 x.compareTo(y) ==0 일 때 x.equals(y) 가 꼭 지켜지는 것이 좋다.
해당 규약이 지켜지지 않는다면 그 사실을 명시해야 한다.
ex) “주의: 이 클래스의 순서는 equals 메서드와 일관되지 않다.”
compareTo 규약을 지키지 못하면 비교를 활용하는 클래스와 어울리지 못한다.
비교를 활용하는 클래스의 예로는 정렬된 컬렉션인 TreeSet과 TreeMap, 검색과 정렬 알고리즘을 활용하는 유틸리티 클래스인 Collections와 Arrays가 있다
compareTo 작성 요령
equals와 비슷하다. 다만 몇 가지 차이점만 주의하면 된다.
- Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 컴파일 타임에 인수 타입이
결정된다. 따라서 equals에서 했던 타입 확인이나 형 변환이 필요 없다.
@Override
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
- null을 인자로 받는다면 NullPointerException을 발생시키면 된다.
❌ 부등호를 사용하는 이전 방식은 피하자
정수 일경우에 < 와 > 사용하여 값을 비교하는 이전 방식은
거추장스럽고 오류를 유발하니 추천하지 않는다.
@Override
public int compareTo(Person other) {
if (this.age < other.age) {
return -1;
} else if (this.age > other.age) {
return 1;
} else {
return 0;
}
}
❌ 값의 차를 기준으로 비교하는 비교자 - 추이성 위배
'값의 차' 를 기준으로 첫번째 값이 두번째 값보다 작으면 음수, 같으면 0, 크면 양수를 반환하는
compareTo나 compare 메서드를 자주 마주할 것이다.
s@Override
public int compareTo(Person other) {
return this.age - other.age;
}
⚠️이 방식은 사용하면 안된다. 정수 오버플로우를 일으키거나 부동소수점 계산 방식에 따른 오류를
낼 수 있다. 또한 다른 방식들보다 월등히 빠르지도 않다.
double diff = 0.1 - 0.2; // 결과는 -0.1이 아닌 -0.10000000000000003이 될 수 있다.
int diff = Integer.MAX_VALUE - Integer.MIN_VALUE; //결과는 잘못된 음수나 양수가 될 수 있다.
⭕ 정적 compare 메서드를 활용한 비교자
자바 7 부터 생기게 된 정적 compare 메서드를 활용한 비교를 추천한다
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
⭕ 기본 타입 필드가 여럿일 때의 비교자
클래스에 핵심필드가 여러 개라면 어느 것을 먼저 비교하느냐가 중요해진다.
가장 핵심적인 필드부터 비교해나가자.
비교 결과가 0이 아니라면 결과를 곧장 반환하고
똑같다면 똑같지 않은 필드를 찾을 때까지 그다음으로 중요한 필드를 비교해나가자
@Override
public int compareTo(PhoneNumber pn) {
int result = Short.compare(areaCode, pn.areaCode); // 가장 먼저 비교할 필드
if (result == 0) {
result = Short.compare(prefix, pn.prefix); // 두번째로 비교할 필드
if (result == 0) {
result = Short.compare(lineNum, pn.lineNum); // 세번째로 비교할 필드
}
}
return result;
}
⭕ Comparator를 활용한 비교자
자바 8부터 Comparator 인터페이스를 사용해 값의 비교가 가능해졌다
Comparable과 차이점은 compare 메서드를 구현해야 하며 인자로 두개의 객체를 받게된다.
public interface Comparator<T> {
int compare(T o1, T o2);
. . .
}
Comparable은 비교 로직이 객체 내부에 묶여 있어 수정 시 클래스 내부를 변경해야 한다.
반면 Comparator는 객체 외부에서 정의할 수 있기 때문에, 클래스 수정 없이
비교 로직을 추가하거나 변경할 수 있습니다.
익명 함수(람다식)로 구현할 수 있기 때문에 코드가 더 간결하고 가독성이 좋아집니다.
// PhoneNumber[] phoneNumbers를 정렬하고자 한다면
1️⃣
Arrays.sort(phoneNumbers, new Comparator<PhoneNumber>() {
@Override
public int compare(PhoneNumber pn1, PhoneNumber pn2) {
int result = Integer.compare(pn1.areaCode, pn2.areaCode);
if (result == 0) {
result = Integer.compare(pn1.prefix, pn2.prefix);
if (result == 0) {
result = Integer.compare(pn1.lineNum, pn2.lineNum);
}
}
return result;
}
});
2️⃣람다식 활용
Arrays.sort(phoneNumbers, (pn1, pn2) -> {
int result = Integer.compare(pn1.areaCode, pn2.areaCode);
if (result == 0) {
result = Integer.compare(pn1.prefix, pn2.prefix);
if (result == 0) {
result = Integer.compare(pn1.lineNum, pn2.lineNum);
}
}
return result;
});
3️⃣메서드체이닝활용
Arrays.sort(phoneNumbers, Comparator
.comparing((PhoneNumber pn) -> pn.areaCode)
.thenComparing(pn -> pn.prefix)
.thenComparing(pn -> pn.lineNum));
메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다.
간결하나 약간의 성능저하가 뒤따른다.
'Backend > Java' 카테고리의 다른 글
[Java] BufferedReader, BufferedWriter 란? (0) | 2025.07.10 |
---|---|
[Java] 정렬 알고리즘 정리 (1) | 2025.07.02 |
[EffectiveJava] Item 55. 옵셔널 반환은 신중히 하라 (1) | 2025.07.01 |
[EffectiveJava] item 61. 박싱된 기본 타입보다는 기본 타입을 사용하라 (0) | 2025.07.01 |
[EffectiveJava] item 64. 객체는 클래스가 아닌 인터페이스로 참조하라 (0) | 2025.07.01 |