Comparator / Comparable 비교 해보기

Comparable 인터페이스

https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html

  • 정렬 수행 시에, 기본적으로 적용되는 정렬의 기준이 되는 메서드를 정의하는 인터페이스
  • 사용 방법
    • Comparable 인터페이스를 구현한 뒤에, 내부에 있는 compareTo 메서드를 원하는 정렬 기준대로 구현하여 사용한다.
class Student implements Comparable<Student> {
	int grade;
	// compareTo 메서드 오버라이드
    @Override
    public int compareTo(Student anotherStudent) {
    	return Integer.compare(grade, anotherStudent.grade);
    }
}
  • 생성되는 객체를 기준으로 나머지 객체들의 정렬 방식을 결정한다.
    • compare 값이 음수인 경우 : 현재 객체가 나머지 객체보다 이전 값
    • compare 값이 0인 경우 : 현재 객체와 나머지 객체의 값이 같음
    • compare 값이 양수인 경우 : 현재 객체가 나머지 객체보다 나중 값
  • Collection 들에 해당 인터페이스를 구현한 객체를 원소로 받는 경우, compareTo에 적용된 정렬 로직 기준이 적용된다.

Comparator 인터페이스

https://docs.oracle.com/javase/8/docs/api/java/util/Comparator.html

  • 정렬 가능한 클래스들의 기본 정렬 기준과 다른 방식으로 정렬하고 싶은 경우
  • 주로 Collections의 정렬 관련 메서드에 Comparator를 익명 객체로 넣어 정렬 기준을 변경하는 데 사용한다.
  • 람다식을 활용할 수 있다.
Comparator<String> comparator = (s1, s2) -> s1.compare(s2);
Map<String, boolean[]> room = new TreeMap<>(comparator);
  • TreeMap과 같은 정렬이 가능한 Collection에 Comparator 객체를 생성자에 추가하여 정렬 기준을 변경이 가능하다.
  • 주의 사항
    • 공식문서에서도 나오듯이, Comparator, Comparable 인터페이스의 equal 조건을 구현할 때 주의해야 한다고 한다.
    • 정렬 기준이 일치하지 않는다면, 자료구조가 정상적으로 동작하지 않을 수 있다고 한다.

 

Arrays.sort(room, new Comparator<String>() {
	@Override
    public int compare(String s1, String s2) {
    	return s1.compare(s2);
    }
}
  • 기본 구현되어있는 정렬 메서드에 익명 객체의 형태로 따로 클래스를 상속받아 구현할 필요없이 정렬 기준만을 재정의하여 사용할 수 있다.

두 인터페이스의 차이점

  • 매개변수의 차이
    • Comparable의 정렬 기준은 메서드를 작성한 클래스와 나머지 객체를 비교한다. 따라서 매개변수가 한 개다.
    • Comparator는 두 개의 매개변수를 받는다. 작성하고 있는 객체가 비교기준이 되지 않는다는 의미이다.
      • 따라서 내가 Comparator를 구현하여 생성한 객체와 관계없이, 비교를 할 수 있는 두 객체만 매개변수로 넘겨준다면 compare 메서드를 통해 비교를 할 수 있다는 의미가 된다.
  • 패키지의 차이
    • Comparable은 java.lang으로, 기본 패키지에 해당한다.
      • 기본 패키지라면.. Integer, String 등 기본 제너릭 클래스는 모두 Comparable 인터페이스를 상속받고 있겠다.
        • 기본적으로 Java는 오름차순으로 정렬기준이 설정되어있다.
      • Comparator는 java.util 패키지로, 사용하려면 패키지 import가 필요하다.
# Java api

public final class Integer extends Number implements Comparable<Integer> {

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
public final
class Character implements java.io.Serializable, Comparable<Character> {

사용

  • 공부를 하면서 설계 방향이나 매개변수의 차이나 사용법의 차이가 있지만, 특정한 기준을 토대로 사용자가 원하는 대로 정렬을 하고자 하는 데에 공통적인 의미가 있는 것으로 생각이 들었다.
  • 보통 Java로 다익스트라 알고리즘을 풀 때 PriorityQueue를 사용하는데, 이 경우 Comparable, Comparator 를 찾아보지 않을까 생각이 든다. 혹은 정렬 알고리즘을 풀거나
    • 다익스트라 알고리즘을 풀다가 갑자기 해당 인터페이스를 정리를 해보고 싶어졌다.
    • 성능적으로 두 개가 얼마나 차이가 있는지는 한 번 확인을 해보고 싶다.
  • 아래는 여러 방식으로 Comparable과 Comparator 인터페이스를 사용해본 결과이다.
    • Comparable 인터페이스는 클래스에 기본적으로 implement 하여 구현을 해주어야한다.
    • Comparator 인터페이스는 익명 객체를 생성하여 여러 개의 정렬 로직을 생성하여 전달해줄 수 있다는 장점이 있겠다.
      • 여러 정렬 기준을 적용이 필요한 경우, Comparator 인터페이스를 사용해주는 것이 더 구현적으로 편리할 것 같다.
static class Number implements Comparable<Number> {
    int number;

    Number(int number) {
        this.number = number;
    }

    @Override
    public int compareTo(Number anotherNumber) {
        return Integer.compare(number, anotherNumber.number);
    }
}

PriorityQueue<Number> comparablePq = new PriorityQueue<>();
  • 사실 그냥 Integer를 사용하는 것과 별 차이가 없으나.. Comparable 인터페이스 상속을 위해 객체를 별도로 생성해주었다.

 

static class NewNumber {
    int number;

    NewNumber(int number) {
        this.number = number;
    }
}

PriorityQueue<NewNumber> comparatorPq = new PriorityQueue<>(new Comparator<NewNumber>() {
    @Override
    public int compare(NewNumber o1, NewNumber o2) {
        return Integer.compare(o1.number, o2.number);
    }
});

Comparator<NewNumber> numberComparator = (n1, n2) -> Integer.compare(n1.number, n2.number);
Comparator<NewNumber> numberComparator2 = Comparator.comparingInt(n -> n.number);

PriorityQueue<NewNumber> comparatorPq2 = new PriorityQueue<>(numberComparator);
PriorityQueue<NewNumber> comparatorPq3 = new PriorityQueue<>(numberComparator2);
  • Comparator의 경우 람다식을 활용하여 더 간단하게 표현할 수 있다는 장점이 있다. 또한 Comparator 인터페이스에는 간단한 정렬 관련 정적 메서드들이 이미 구현이 되어있다.
public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}

public static <T> Comparator<T> comparingLong(ToLongFunction<? super T> keyExtractor) {
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> Long.compare(keyExtractor.applyAsLong(c1), keyExtractor.applyAsLong(c2));
}
    
public static<T> Comparator<T> comparingDouble(ToDoubleFunction<? super T> keyExtractor) {
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> Double.compare(keyExtractor.applyAsDouble(c1), keyExtractor.applyAsDouble(c2));
}

 

  • 실행 결과는 Comparable과 Comparator 모두 동일했다.
    • 두 인터페이스의 차이와 구현 간단도에 따라 취사선택하여 사용하면 될 것 같다.
    • 자세한 성능 차이나, 구현 방식에 대해서는 계속 생각해봐야겠다. 차이점이 느껴지는 부분이 있는 경우 추가적으로 해당 글에 정리해야겠다. ( 혹시나 알고 계신 분은 댓글 부탁드립니다.. )
for(int i=0; i<15; i++) {
    int rand = new Random().nextInt(1000);
    comparablePq.offer(new Number(rand));
    comparatorPq.offer(new NewNumber(rand));
}

for(int i=0; i<15; i++) {
    System.out.println("Comparable pq : " + comparablePq.poll().number + " vs Comparator pq : " + comparatorPq.poll().number);
}

Comparable pq : 92 vs Comparator pq : 92
Comparable pq : 220 vs Comparator pq : 220
Comparable pq : 233 vs Comparator pq : 233
Comparable pq : 264 vs Comparator pq : 264
Comparable pq : 267 vs Comparator pq : 267
Comparable pq : 273 vs Comparator pq : 273
Comparable pq : 355 vs Comparator pq : 355
Comparable pq : 451 vs Comparator pq : 451
Comparable pq : 485 vs Comparator pq : 485
Comparable pq : 489 vs Comparator pq : 489
Comparable pq : 539 vs Comparator pq : 539
...