요즘 너무 바빠서 이번에도 가벼운 주제로 가져와 봤습니다.
사실 SOLID의 O를 이어가는 게 가장 이상적이었겠지만, 시간이 부족한 관계로…
한 달 전쯤같이 공부하는 친구가 Comparable과 Comparator가 어렵고 헷갈린다고 하여 설명해 주었습니다.
그 뒤로도 주변에서 코딩테스트 언어를 선택할 때 “자바 뭐 Comparator…? 그거 복잡하잖아”와 같은 말들을 듣게 되었습니다.
그래서 Comparable과 Comparator가 어렵지 않다는 주제로 블로그 작성을 기획하게 되었습니다.
언제 필요한데?
자바는 Arrays.sort()나 Collections.sort()같은 정렬 기능을 제공합니다. 그래서 우리는 다음처럼 int 배열과 Integer 리스트를 정렬할 수 있습니다.
// 편의상 원소 삽입 및 값 할당은 생략했습니다.
int[] numsArray = new int[10];
Arrays.sort(numsArray);
List<Integer> numsList = new ArrayList<>();
Collections.sort(numsList);
직접 정렬 알고리즘을 작성하지 않아도 되니 편하네요!
하지만 곧 있으면 두 가지 문제를 마주하게 됩니다.
어? 자바 정렬은 오름차순 정렬이네.. 내림차순 정렬은 어떻게 하지?
어.. 내가 만든 객체를 정렬하고 싶은데, 어떻게 하지?
이때 우리는 Comparable과 Comparator가 필요하게 됩니다!
sort 함수를 살펴보자
Arrays와 Collections 클래스는 다음과 같은 정렬 함수들을 제공합니다.
Arrays 클래스는 primitive type과 Object에 대해 정렬 기능을 제공하고 Collections는 제네릭 파라미터 타입 T에 대해 정렬 기능을 제공하고 있습니다.
위 사진을 보면 Arrays는 primitive type에 대한 sort 함수를 오버로딩을 통해 각각 제공하고 있습니다.
(primitive type은 제네릭 파라미터 타입으로 사용할 수 없으므로 Collections의 sort 함수에는 사용할 수 없습니다)
즉 자바는 int, byte, char, long, float, short, double을 정렬하는 방법을 이미 알고 있습니다. 그래서 우리에게 정렬 함수를 제공할 수 있는 것이죠.
그럼 두 함수가 남습니다.
// Arrays
sort(Object[] a)
sort(T[] a, Comparator<? super T> c
// Collections
sort(List<T> list)
sort(List<T> list, Comparator<? super T> c
Arrays와 Collections가 비슷한 형태의 API를 제공하는 것으로 보입니다.
하나는 리스트, 배열만을 전달하고 하나는 Comparator를 함께 전달합니다.
배열만 전달하는 함수는 다음과 같습니다.
/*
지정된 객체 배열을 해당 요소들의 자연 순서에 따라 오름차순으로 정렬합니다.
배열의 모든 요소는 Comparable 인터페이스를 구현해야 합니다.
또한, 배열의 모든 요소는 서로 비교 가능해야 합니다
(즉, 배열의 어떤 요소 e1과 e2에 대해 e1.compareTo(e2) 메서드가 ClassCastException을 발생시키지 않아야 합니다).
ClassCastException – 배열에 서로 비교할 수 없는 요소들(예: 문자열과 정수)이 포함되어 있는 경우.
*/
public static void sort(Object[] a) { ... }
리스트만 전달하는 함수는 다음과 같습니다.
// ClassCastException – 리스트에 서로 비교할 수 없는 요소들(예: 문자열과 정수)이 포함되어 있는 경우.
@SuppressWarnings("unchecked")
public static <T extends Comparable<? super T>> void sort(List<T> list) { ... }
정확히 무슨 말인지는 몰라도 매개변수가 될 배열, 리스트의 원소가 Comparable이라는 것을 구현해야 한다는 것을 알 수 있습니다.
그러므로 남은 두 개의 함수는 다음과 같습니다.
- 매개변수로 Comparable을 구현하는 원소를 가진 배열 / 리스트를 넘김
- 매개변수로 배열 / 리스트와 Comparator를 넘김
그래서 Comparable, Comparator가 뭔데?
위에서 봤듯이 Arrays 클래스는 primitive type에 대한 정렬 방법을 알고 있습니다. 그러나 Object와 제네릭 파라미터 타입 T를 정렬하는 방법은 모릅니다.
자바 API의 입장에선 이미 정해져 있는 primitive type과 달리 우리가 만들 객체는 어떻게 생겼을지 전혀 예상할 수 없기 때문이죠.
그래서 우리는 자바 API에게 “우리가 만든 객체는 이렇게 정렬해 줘!”하고 말해줘야 합니다. 그리고 자바에게 정렬 방법을 알려주는 두 가지 방법이 바로 Comparable과 Comparator입니다.
우선 이들은 네이밍이 굉장히 직관적입니다.
Comparable → Compare + able → 비교 가능한
Comparator → Compare + tor → 비교하는 사람
네이밍을 생각하고 다시 보면 우리는 sort 함수에게 “비교가능한” 객체를 넘겨주거나, 객체와 해당 객체를 “비교하는 사람”을 넘겨주는 것이라고 할 수 있습니다.
어떤가요? 좀 와닿으시나요?
이제 Comparable과 Comparator가 어떻게 생긴 녀석들인지 봅시다.
Comparable
public interface Comparable<T> {
public int compareTo(T o);
}
Comparable은 굉장히 간단하게 생긴 인터페이스입니다.
compareTo라는 하나의 메서드만 구현해주면 됩니다.
Comparable은 “비교 가능한” 것이므로 이를 구현한 객체 또한 그럴 것입니다.
그 “비교”를 가능하게 해주는 것이 compareTo 메서드입니다.
이해하기 쉽도록 구현부를 보고 이야기 해봅시다.
이름과 나이를 갖는 비교 가능한 Person 클래스는 다음과 같습니다. (나이가 비교 기준이다)
public class Person implements Comparable<Person> {
private String name;
private int age;
...
@Override
public int compareTo(Person other) {
if(age > other.age) { // 내가 더 크면 양수 반환
return 1;
} else if(age < other.age) { // 내가 더 작으면 음수 반환
return -1;
} else { // 같으면 0 반환
return 0;
}
}
}
compareTo를 구현하는 방법은 정말 간단합니다.
- 내가(this) 매개변수로 들어온 비교 대상 객체(other)보다 크면 양수를 반환
- 내가(this) 매개변수로 들어온 비교 대상 객체(other)보다 작으면 음수를 반환
- 내가(this) 매개변수로 들어온 비교 대상 객체(other)와 같으면 0을 반환
그리고 저 compareTo 메서드를 다음처럼 간단하게도 정의해볼 수 있습니다.
@Override
public int compareTo(Person other) {
return age - other.age;
}
@Override
public int compareTo(Person other) {
return Integer.compare(age, other.age);
}
더하기 빼기 연산이 가능한 경우, 첫 번째 예시처럼 나에서 상대를 빼면 반환값을 바로 구할 수 있습니다.
(반대로 내림차순 정렬을 하고 싶다면 상대에서 나를 뺀 값을 반환하면 됩니다)
다음으로 Integer와 같은 primitive type의 wrapper 클래스들은 compare라는 함수를 제공합니다.
Integer 또한 Comparable이므로 CompareTo를 구현하며 그 안에서 compare 함수를 사용합니다.
그럼 나이 말고 이름순(출석순)으로 정렬하고 싶다면 어떻게 해야할까요?
String 클래스 또한 Comparable이므로 그냥 compareTo를 호출해주면 됩니다.
@Override
public int compareTo(Person other) {
return name.compareTo(other.name);
}
그럼 이제 Comparable을 사용해서 정렬을 해봅시다.
Comparable을 구현한 Person과 Comparable을 구현하지 않은 Person을 만들었습니다.
그리고 sort 함수에 넘겨보았습니다.
Comparable을 구현한 Person을 담은 리스트는 정렬 가능하지만,
Comparable을 구현하지 않은 Person을 담은 리스트는 정렬이 불가능합니다.
정리하면 Comparable은 다음과 같습니다.
- Comparable을 구현하여 객체를 비교 가능하게 만들자.
- compareTo 메서드만 구현하면 된다.
- 두 객체를 비교한 결과를 int로 반환한다.
어떠신가요? 아직도 Comparable이 어려우신가요?
음.. 그렇다면 댓글 남겨주십쇼…
Comparator
public interface Comparator<T> {
int compare(T o1, T o2);
...
}
Comparator는 여러 디폴트 메서드가 존재하면서 Comparable에 비해서는 기능이 많은 인터페이스입니다.
그러나 우리가 구현해야할 것은 compare 메서드 하나뿐입니다.
compare의 구현 방법은 Comparable의 compareTo와 유사합니다.
이름으로 정렬하는 Comparator와 나이로 정렬하는 Comparator를 각각 만들어보았습니다.
class PersonNameComparator implements Comparator<UnComparablePerson> {
@Override
public int compare(UnComparablePerson o1, UnComparablePerson o2) {
return o1.getName().compareTo(o2.getName());
}
}
class PersonAgeComparator implements Comparator<UnComparablePerson> {
@Override
public int compare(UnComparablePerson o1, UnComparablePerson o2) {
return o1.getAge() - o2.getAge();
}
}
이제 만든 Comparator를 사용해보겠습니다.
List<UnComparablePerson> unComparablePeople = Arrays.asList(
new UnComparablePerson("백천", 30),
new UnComparablePerson("청명", 20),
new UnComparablePerson("조걸", 23),
new UnComparablePerson("유이설", 26),
new UnComparablePerson("윤종", 25)
);
Collections.sort(unComparablePeople, new PersonNameComparator());
System.out.println(unComparablePeople);
Collections.sort(unComparablePeople, new PersonAgeComparator());
System.out.println(unComparablePeople);
결과는 다음과 같습니다. 첫 줄은 이름 순으로, 둘째 줄은 나이 순으로 정렬되었습니다.
혹은 따로 Comparator 클래스를 정의하지 않고 익명 클래스로도 처리가 가능합니다.
Collections.sort(unComparablePeople, new Comparator<UnComparablePerson>() {
@Override
public int compare(UnComparablePerson o1, UnComparablePerson o2) {
return o1.getName().compareTo(o2.getName());
}
});
만약 비교 기준이 Comparable이라면 Comparator의 정적 메서드인 comparing을 사용해볼 수도 있습니다.
// String이 Comparable이기에 가능합니다.
Collections.sort(unComparablePeople, Comparator.comparing(UnComparablePerson::getName));
// int, long, double에 대해서는 comparing 함수가 따로 존재합니다.
Collections.sort(unComparablePeople, Comparator.comparingInt(p -> p.getAge()));
comparing과 thenComparing을 활용하면 여러 기준을 쉽게 적용할 수도 있습니다.
나이를 기준으로 먼저 정렬하고 나이가 같으면 이름을 기준으로 정렬했습니다.
List<UnComparablePerson> unComparablePeople = Arrays.asList(
new UnComparablePerson("백천", 30),
new UnComparablePerson("당패", 30),
new UnComparablePerson("조걸", 23),
new UnComparablePerson("곽회", 23),
new UnComparablePerson("윤종", 25),
new UnComparablePerson("남궁도위", 25)
);
Collections.sort(
unComparablePeople,
Comparator.comparingInt(UnComparablePerson::getAge)
.thenComparing(UnComparablePerson::getName)
);
System.out.println(unComparablePeople);
물론 comparing을 사용하지 않고도 가능합니다.
다음과 같이 나이를 기준으로 정렬해서 반환하고, 나이가 같다면 이름으로 비교해서 반환합니다.
Collections.sort(unComparablePeople, new Comparator<UnComparablePerson>() {
@Override
public int compare(UnComparablePerson o1, UnComparablePerson o2) {
if(o1.getAge() > o2.getAge()) return 1;
else if(o1.getAge() < o2.getAge()) return -1;
else {
return o1.getName().compareTo(o2.getName());
}
}
});
또한 Comparator는 Functional Interface이므로 익명 클래스를 사용하지 않고 compare 함수를 람다로 표현할 수 있습니다.
List<UnComparablePerson> unComparablePeople = new ArrayList<>();
Collections.sort(unComparablePeople, (p1, p2) -> p1.getAge() - p2.getAge());
Collections.sort(unComparablePeople, (p1, p2) -> p1.getName().compareTo(p2.getName()));
자바8(1.8)부터 람다와 함수형 인터페이스가 지원되었습니다. 함수형 인터페이스란 추상 메서드가 하나 뿐인 인터페이스입니다. 그리고 함수형 인터페이스는 람다 표현으로 대체될 수 있습니다.
물론 Comparator 내부에는 comparing과 같이 다른 함수들도 있지만, 모두 디폴트 메서드나 정적 메서드이며 추상 메서드는 compare 하나 뿐입니다.
그런데 이렇게 코드를 작성하면 인텔리제이는 아래처럼 comparing 함수 사용을 추천합니다. 본인이 편한걸 쓰면 될 것 같습니다.
정리하면 Comparator는 다음과 같습니다.
- Comparator를 구현해 비교 불가능한 객체를 비교하는 사람을 만들자.
- compare만 구현하면 된다.
- 함수형 인터페이스이기에 람다 표현으로 대체할 수 있다.
- Comparable의 compareTo와 같이 두 객체를 비교한 결과를 int로 반환한다.
어떠신가요? 아직 Comparator가 어려우신가요?
물론 Comparable보다는 내용이 많아 헷갈리실 수 있습니다. 그러나 천천히 다시 읽어보시면 충분히 이해가 되실거라 믿습니다.
문제 해결?
그럼 처음에 가졌던 의문 두 가지가 모두 풀렸습니다.
- 내림차순 정렬
- 직접 만든 객체 정렬
아래 코드처럼 우리는 Comparator와 Comparable을 사용해 내림차순과 내가 만든 객체 정렬이 가능해졌습니다!
// 나이 기준 내림차순 정렬
Collections.sort(unComparablePeople, (p1, p2) -> p2.getAge() - p1.getAge());
// 이름 기준 내림차순 정렬
public class ComparablePerson implements Comparable<ComparablePerson> {
private String name;
private int age;
public ComparablePerson(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(ComparablePerson other) {
return other.name.compareTo(name);
}
...
}
하지만 여기서 내림차순 정렬을 하는 다른 방법도 있습니다.
- 비교 불가능한 객체의 경우 comparing 함수와 reversed 함수를 체이닝하여 사용합니다.
Collections.sort(unComparablePeople, Comparator.comparing(UnComparablePerson::getName).reversed());
- Comparable을 구현한 객체의 경우 reverseOrder를 추가 파라미터로 넘겨줍니다.
Collections.sort(comparablePeople, Comparator.reverseOrder());
둘 중에 무엇을 사용해야 하나요?
이제 우리는 Comparable도 알았고, Comparator도 알았습니다.
그런데 막상 코드를 작성하려니 둘 중에 무얼 사용해야 할지 고민될 수 있습니다.
개인의 취향이지만, 그래도 혼란스러울 분들을 위해 적당한 가이드를 제시해보려고 합니다.
- 한 객체에 대해 기준이 하나다 → 둘 중에 편한 거 사용
- 한 객체에 대해 기준이 여러 개다 → Comparator 사용
만약 Person을 때에 따라 다른 기준(이름 or 나이)으로 정렬해야 한다면, Comparable로는 불가능합니다.
아까 위에서 예시로 보았던 것처럼 Comparator를 사용해야 합니다.
Comparable은 이미 객체에 정렬 기준이 정해져 있기 때문입니다.
마무리
이렇게 Comparable과 Comparator에 대해 알아보았습니다.
이해가 됐을까요? 혹은 오히려 더 혼란스러워졌을까요? 그렇다면 제가 죄송합니다...
제가 이 글을 통해 하고 싶었던 말은 자바의 Comparable과 Comparator를 활용하는 것이 전혀 어렵지 않다는 것입니다.
저 또한 자바를 처음 접했을 때는 혼란스러웠습니다.
그러나 살짝 보고 겁먹기보다 한 번 제대로 보면 전혀 어렵지 않을 것이라고 말씀드릴 수 있습니다.
이상으로 글을 마치겠습니다!