우테코에서 출석 미션을 하는데 Comparable을 사용할 일이 많았다.
페어프로그래밍 하면서 페어에게 설명해주고 싶었는데 항상 관성적으로 써오던 인터페이스라
그냥 '객체를 서로 비교할 때 사용하시면 좋습니다'라는 말 밖에 생각나지 않았다.
그렇다. Comaprable에 대해 제대로 공부해본 적이 없었다.
그래서 지금 하고자 한다.
1. Comparable은 무엇인가?
Comaprable은 이 객체와 다른 객체를 비교할 수 있다는 것을 의미하는 인터페이스이다.
`java.lang.Comparable<T>` 형태로 존재하며, T라는 제네릭 타입을 통해 "어떤 타입을 비교할 건지"를 지정해준다.
자바 API 문서를 확인해보면, Comparable은 다음과 같이 정의되어 있다.
Comparable 주석을 해석해보면 아래와 같다.
이 인터페이스는 이를 구현하는 각 클래스의 객체들에 대한 전체 순서(total ordering) 를 부여한다.
이 순서는 해당 클래스의 자연 순서(natural ordering) 라고 하며, 클래스의 compareTo 메서드는 자연 비교 메서드(natural comparison method) 라고 한다.
❓전체 순서(Total Ordering)
전체 순서(total ordering) 란, 집합 내 모든 요소들 간에 일관된 비교 기준이 존재하는 것을 의미한다.
즉, 집합 안의 어떤 두 요소라도 비교가 가능하고, 항상 "작다(<), 크다(>), 같다(=)"라는 관계가 성립해야 한다.
예를 들어, 숫자 집합 {3, 1, 4, 2}를 오름차순으로 정렬하면 {1, 2, 3, 4}가 되는데,
여기서 모든 숫자 간의 비교 기준이 "작다(<) 또는 크다(>)" 로 확실하게 정해져 있으므로 전체 순서를 가진다고 볼 수 있다.
❓자연 순서(Natural Ordering)
자연 순서(natural ordering) 는 특정 클래스가 기본적으로 따르는 정렬 기준을 의미한다.
예를 들어, Integer 클래스의 자연 순서는 숫자의 크기순(오름차순) 이고, String 클래스의 자연 순서는 사전순 이다. 이처럼, 자연 순서는 그 클래스의 compareTo 메서드가 정의하는 순서를 따르게 된다.
2. 왜 필요한가?
우리가 나이순으로 회원 리스트를 나열하고 싶거나 쇼핑몰 상품을 가격 순으로 보고 싶다는 등 객체들을 "정렬"하고 싶을 때 `Collections.sort()` 메서드나 `Arrays.sort()`메서드를 사용할 수 있는데 이때 `Comparable`을 구현해두면 어떤 기준으로 정렬해야 하는지 자바가 알 수 있게 된다.
쉽게 말해, "내가 원하는 기준으로 객체를 비교/정렬하려면, `Comparable`을 구현해 `compareTo`를 작성하면 된다." 라고 기억하면 된다.
이펙티브 자바(Item 14)에서는 아래와 같이 표현한다.
좁쌀만 한 노력으로 코끼리만 한 큰 효과를 얻는 것이다.
3. 예시
간단한 Person 객체이다.
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public int compareTo(Person other) {
return this.age - other.age;
}
}
위 예시에는 `Person`클래스가 `Comparable<Person>`을 implements했고, `compareTo(Person other)` 메서드를 오버라이딩 했다. 그 결과 "나이"가 비교 기준이된다.
만약 여기서 다른 기준인 `name`으로 정렬하고 싶다면 아래처럼 해주면 된다.
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
문자열끼리 비교하면 compareTo를 바로 사용할 수 있다. (객체도 가능)
이렇게 바꿔주기만 하면, `Collections.sort()` 호출 시 이름 사전 순으로 정렬된다.
4. Comparable vs Comparator
사실 이번 기회에 확실히 둘의 차이를 알게되었다.
- `Comparable`은 "클래스 자체"에 비교 기준을 정의해두는 방식이다.
- 즉, 어떤 클래스가 "나를 어떻게 정렬할지"를 스스로 알고 있게 만든다.
- 아주 쉽게 말하면 객체가 스스로 정렬 기준을 정한다고 생각하면 좋다.
- `Comparator`는 비교 로직을 따라 분리해둘 때 사용하는 인터페이스이다.
- 즉, "정렬 방법을 다르게 하고 싶을 때 따로 만들게!" (정렬 기준을 외부에서 따로 정함)
- 예를 들어, "나이 순 정렬", "이름 순 정렬" 등 여러 가지 기준을 클래스 외부에서 정의해야 한다면, `Comparator`를 이용해 별도의 클래스로 만들어줄 수 있다.
- 쉬운 예로 들자면 기존에 Comparable을 통해서 사람을 나이 순으로 정렬했지만 이름순으로도 정렬하고 싶다면? 그때 바로 Comparator을 사용하면 된다.
Comparator<Person> nameComparator = Comparator.comparing(person -> person.name);
people.sort(nameComparator);
System.out.println(people);
// [Alice (30), Bob (25), Charlie (35)]
5. Comparator 인터페이스
자바 8에서 `Comparator` 인터페이스가 일련의 비교자 생성 메서드와 메서드 연쇄방식으로 비교자를 생성할 수 있게 되었다.
- 비교자들은 `compareTo` 메서드를 구현하는데 활용될 수 있다.
- 이 방식은 간결하지만 약간의 성능 저하가 발생한다.
- 자바의 정적 임포트 기능을 활용하면 정적 비교자 생성 메서드들을 그 이름만으로 사용할 수 있어 코드가 훨씬 깔끔해진다.
- `Comparator`는 `comparingInt`와 `thenComparingInt`등의 숫자용 기본 타입을 커버하는 보조 생성 메서드들을 가지고 있다.
- `comparing`와 `thenComparing`이란 객체 참조용 비교자 생성 메서드 또한 가지고 있다.
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);
}
- 위 코드는 비교자 생성 메서드 2개를 이용해 비교자를 생성한다.
- 첫 번째는 comparingInt 두 번째는 thenCompaingInt이다.
- comparingInt는 객체 참조를 int 타입 키에 매핑하는 키 추출 함수를 인수로 받아 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메소드이다.
- thenComparingInt는 Comparator의 인스턴스 메서드로, int 키 추출 함수를 입력받아 다시 비교자를 반환한다.
- thenComparingInt는 연달아 호출이 가능하다.
- 자바의 타입 추론 능력으로 인해 호출할 때 타입을 명시하지 않는다.
한걸음 더
📚 Reference
- https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html
'JAVA' 카테고리의 다른 글
isBlank() vs isEmpty() (0) | 2025.02.28 |
---|