본문 바로가기
JAVA

Java - Comparable, Comparator

by 봄석 2019. 10. 15.

Java Comparable, Comparator

"caAbBC"와 같은 알파벳 문자열을 "aAbBcC"와 같은 알파벳 순서로 정렬하는 코드를 작성하려면 어떻게 해야할까요 ?

 

위 문제를 위한 코드를 자바로 작성한다면, 아마도 정렬을 실실하는 코드를 작성하되

그 안에서 문자들에 대한 비교 연산을 새로 정의하는 형태로 작성해야 할 것입니다.

 

위 문제에 핵심은 '비교연산' 을 구현하는데에 있습니다.

그냥 단순히 문자열을 char 배열로 바꾸고 각 char 형으로 정렬하고자 한다면 비교하는데 쓰이는 값이 ASCII값이므로 

자연율로 정렬해버리게됩니다. ABCDabcd와 같은 형태가 되겠죠

 

위처럼 자연율(일반적인 정렬순서)가 아니라 사용자가 새롭게 정렬 순서를 정의하고 싶거나 오브젝트를 정렬할 때

오브젝트 정렬에 사용 될 값을 지정하는 역할을 하는 인터페이스가 바로 Comparable과 Comparator입니다.

 

 

Comparable vs Comparator

그렇다면 Comparable과 Comparator의 차이는 무었일까요 ??

 

우선 각 인터페이스를 알아보도록 하겠습니다.

Comparable 인터페이스의 원형

public interface Comparable<T> {
    int compareTo(T var1);
}

 

수정 자 및 유형 방법 및 설명
int compareTo(T o)

이 객체를 지정된 객체와 순서와 비교합니다.

Comparator 인터페이스의 원형

@FunctionalInterface
public interface Comparator<T> {
    
    int compare(T o1, T o2);

    boolean equals(Object obj);

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }

    default Comparator<T> thenComparing(Comparator<? super T> other) {
        Objects.requireNonNull(other);
        return (Comparator<T> & Serializable) (c1, c2) -> {
            int res = compare(c1, c2);
            return (res != 0) ? res : other.compare(c1, c2);
        };
    }

    default <U> Comparator<T> thenComparing(
            Function<? super T, ? extends U> keyExtractor,
            Comparator<? super U> keyComparator)
    {
        return thenComparing(comparing(keyExtractor, keyComparator));
    }

    default <U extends Comparable<? super U>> Comparator<T> thenComparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        return thenComparing(comparing(keyExtractor));
    }

    default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor) {
        return thenComparing(comparingInt(keyExtractor));
    }

    default Comparator<T> thenComparingLong(ToLongFunction<? super T> keyExtractor) {
        return thenComparing(comparingLong(keyExtractor));
    }
    
    default Comparator<T> thenComparingDouble(ToDoubleFunction<? super T> keyExtractor) {
        return thenComparing(comparingDouble(keyExtractor));
    }
    
    public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
        return Collections.reverseOrder();
    }

    @SuppressWarnings("unchecked")
    public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
        return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
    }

    public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
        return new Comparators.NullComparator<>(true, comparator);
    }

    public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) {
        return new Comparators.NullComparator<>(false, comparator);
    }

    public static <T, U> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor,
            Comparator<? super U> keyComparator)
    {
        Objects.requireNonNull(keyExtractor);
        Objects.requireNonNull(keyComparator);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
                                              keyExtractor.apply(c2));
    }

 
    public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor)
    {
        Objects.requireNonNull(keyExtractor);
        return (Comparator<T> & Serializable)
            (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
    }

    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));
    }
}

Comprator Interface는 추상메소드 compare()을 가지는 functional Interface 입니다.

compare() 메소드 외 에도 여러개의 default메소드와 static메소드를 가지고 있습니다.

 

수정 자 및 유형 방법 및 설명
int compare(T o1, T o2)

순서에 대한 두 가지 인수를 비교합니다.

static <T,U extends Comparable<? super U>>
Comparator<T>
comparing(Function<? super T,? extends U> keyExtractor)

Comparabletype에서 정렬 키 를 추출하고 해당 정렬 키로 비교하는를 T반환 하는 함수를 허용합니다 Comparator<T>.

static <T,U> Comparator<T> comparing(Function<? super T,? extends U> keyExtractor, Comparator<? super U> keyComparator)

형식에서 정렬 키를 추출 하고 지정된 정렬 키를 사용하여 해당 정렬 키 T와 Comparator<T>비교하는를 반환 하는 함수를 허용 합니다 Comparator.

static <T> Comparator<T> comparingDouble(ToDoubleFunction<? super T> keyExtractor)

doubletype에서 정렬 키 를 추출하고 해당 정렬 키로 비교하는를 T반환 하는 함수를 허용합니다 Comparator<T>.

static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor)

inttype에서 정렬 키 를 추출하고 해당 정렬 키로 비교하는를 T반환 하는 함수를 허용합니다 Comparator<T>.

static <T> Comparator<T> comparingLong(ToLongFunction<? super T> keyExtractor)

longtype에서 정렬 키 를 추출하고 해당 정렬 키로 비교하는를 T반환 하는 함수를 허용합니다 Comparator<T>.

boolean equals(Object obj)

다른 객체가이 비교기와 "동일한 지"여부를 나타냅니다.

static <T extends Comparable<? super T>>
Comparator<T>
naturalOrder()

Comparable객체를 자연 순서대로 비교하는 비교자를 반환합니다 .

static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator)

 null이 아닌 것으로 간주되는 널 친화적 인 비교 자를 리턴합니다.

static <T> Comparator<T> nullsLast(Comparator<? super T> comparator)

 null이 아닌 것보다 큰 것으로 간주되는 널 친화적 인 비교 자를 리턴합니다.

default Comparator<T> reversed()

이 비교 자의 역순을 부과하는 비교자를 반환합니다.

static <T extends Comparable<? super T>>
Comparator<T>
reverseOrder()

자연 순서 의 역순을 부과하는 비교자를 반환합니다 .

default Comparator<T> thenComparing(Comparator<? super T> other)

다른 비교기와 함께 사전 순서를 비교합니다.

default <U extends Comparable<? super U>>
Comparator<T>
thenComparing(Function<? super T,? extends U> keyExtractor)

Comparable정렬 키 를 추출하는 함수가 포함 된 사전 순서 순 비교자를 리턴합니다 .

default <U> Comparator<T> thenComparing(Function<? super T,? extends U> keyExtractor, Comparator<? super U> keyComparator)

지정된와 비교할 키를 추출하는 함수가 포함 된 사전 순서 비교기를 리턴합니다 Comparator.

default Comparator<T> thenComparingDouble(ToDoubleFunction<? super T> keyExtractor)

double정렬 키 를 추출하는 함수가 포함 된 사전 순서 순 비교자를 리턴합니다 .

default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor)

int정렬 키 를 추출하는 함수가 포함 된 사전 순서 순 비교자를 리턴합니다 .

default Comparator<T> thenComparingLong(ToLongFunction<? super T> keyExtractor)

long정렬 키 를 추출하는 함수가 포함 된 사전 순서 순 비교자를 리턴합니다 .

...더보기

공통점

- 정렬의 기준을 정합니다.

공통점

- Comparable : 객체간의 일반적인 정렬이 필요할때 사용 , compareTo 메소드 사용

- Comparator : 객체간의 특정한 정렬이 필요할때 ,  compare() 메소드 사용

 

 

 

자세하게 예를 보고 알아보도록 하겠습니다.

 

Comparable Example

  @Test
    public void basicSortWithListDefaultMethod(){
        List<String> list = new ArrayList<>();

        list.add("E");
        list.add("A");
        list.add("C");
        list.add("B");
        list.add("D");

        Collections.sort(list);

        List<String> actual = Arrays.asList("A","B","C","D","E");

        Assert.assertThat(actual, CoreMatchers.equalTo(list));
    }

Collection.sort로 정렬한것을 문자리스트를 비교하는 예입니다.

public static <T extends Comparable<? super T>> void sort(List<T> list) {
        list.sort((Comparator)null);
    }
default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, c);
        ListIterator<E> i = this.listIterator();
        Object[] var4 = a;
        int var5 = a.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            Object e = var4[var6];
            i.next();
            i.set(e);
        }

    }
   public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else if (Arrays.LegacyMergeSort.userRequested) {
            legacyMergeSort(a, c);
        } else {
            TimSort.sort(a, 0, a.length, c, (Object[])null, 0, 0);
        }

    }

여기서 Collection의 sort메소드의 제네릭 부분을 보게되면

 <T extends Comparable<? super T>>

Comparable 을 구현한 클래스를 허용할 수 있는것을 볼 수있습니다.

 

Collection의 스테틱메소드 sort의 함수 body를 보게되면

List의 default 메소드 sort를 다시 호출하고(list.sort((Comparator)null);) 그안에서 

다시 Arrays의 스테틱 메소드를 호출하게됩니다. (  Arrays.sort(a, c);)

그안에서는 Comparable이 정의되어있는지 아닌지를 따라 기본정렬을 할지 

상용자가 정의한 대로 정렬을 할지 결정하여 정렬하는 것 입니다.

 

 

그렇다면 Comparable을 구현하는 Person클래스를 통해 정렬해보도록 하겠습니다.

class Person implements Comparable<Person> {
        private int age;
        private String name;
        private int salary;

        public Person(int age, String name) {
            this.age = age;
            this.name = name;
        }

        public Person(int age, String name, int salary) {
            this.age = age;
            this.name = name;
            this.salary = salary;
        }

        @Override
        public int compareTo(Person person) {
            if (age == person.age)
                return 0;
            else if (age < person.age)
                return -1;
            else
                return 1;
            /*
              아래와 같이 사용할 비교할 수도 있음
              return Integer.compare(age, person.age);
              return age - person.age;
              */
        }
    }

    @Test
    public void sortWithComparableImplPerson() {
List<Person> list = new ArrayList<>() {{
        add(new Person(10, "a", 3000));
        add(new Person(12, "b", 2000));
        add(new Person(8, "c", 2500));
        add(new Person(13, "d", 4000));
    }};

        Collections.sort(list);

        List<String> actual = Arrays.asList("c", "a", "b", "d");
        List<String> expect = list.stream()
                .map(p -> p.name)
                .collect(Collectors.toList());

        Assert.assertThat(actual, CoreMatchers.equalTo(expect));
    }

 

위 Person 을 age순으로 내림차순으로 정렬하고자 한다면 

list.sort(Collections.reverseOrder());

혹은 

@Override
public int compareTo(Person person) {
     return person.age- age;
}

을 바꾸어 줄 수 있습니다.

 

 

 

Comparator Example

그렇다면 Comparator 는 언제 사용하는게 좋을까요 ??

Comparator는 일반적이지 않은 문자열의 길이 순으로 보고싶다던지 Comparable 로 구현한 것 말고 

다른 기준으로 정렬하고싶을때 사용할 수 있습니다.

 

예를 들어 위의 예에서 정렬한대로 나이순으로 정렬해놓은 Person들을

연봉순으로 보고싶다던지 하는 경우가 있을 것입니다.

 

이럴 때마다 compareTo를 수정하게되면 비효율 적이게 되겠죠.

 

이럴때 Comparator를 사용하여 정렬이 정렬에 대한 기준을 정해줄 수 있습니다.

 

 

 

  @Test
    public void comparatorSort() {
    	List<Person> list = new ArrayList<>() {{
    	    add(new Person(10, "a", 3000));
    	    add(new Person(12, "b", 2000));
    	    add(new Person(8, "c", 2500));
    	    add(new Person(13, "d", 4000));
    	}};
      
          List<String> expect = Arrays.asList("d", "a", "c", "b");

        list.sort((p1, p2) -> p2.salary - p1.salary);
        
        List<String> actual = list.stream()
                .map(p -> p.name)
                .collect(Collectors.toList());

        Assert.assertThat(expect, CoreMatchers.equalTo(actual));
    }

 

Person은 이미 Comparable을 구현하고 있는 클래스이지만,

Comparator를 이용하여 다른 기준으로 정렬을 할 수있습니다.

 

list.sort((p1, p2) -> p2.salary - p1.salary);

위 코드는 Compartor를 람다를통해 인스턴스로 만든것입니다.

 

 

 

 

 

정리

Comparable : 객체 간의 일반적인 정렬이 필요할 때, Comparable 인터페이스를 확장해서 정렬의 기준을 정의하는 compareTo() 메서드를 구현한다.

Comparator : 객체 간의 특정한 정렬이 필요할 때, Comparator 인터페이스를 확장해서 특정 기준을 정의하는 compare() 메서드를 구현한다.

댓글