언어/Java

Comparable vs Comparator 비교

부리부리부리부리 2023. 8. 7. 23:01

개요

Comparable과 Comparator 모두 정렬할 때 사용되는 인터페이스들이다.

그냥 Arrays.sort(), Collections.sort()를 쓰면 되지않나? 라는 의문이 들었지만

Comparable과 Comparator를 구현할 수 있다면 더 객체지향적이고 자유롭게 정렬할 수 있다.  

각각 어떤 내용인지 알아보고 어떤 차이점이 있는지 알아보자.

 

정렬

 

먼저  정렬이란, 요소를 특정 기준에 대한 내림차순 또는 오름차순으로 배치하는 것을 뜻한다.

Java에서는 순서를 가지는 Collection들만 정렬 가능하다.

  • List 계열
  • Set에서는 SortedSet의 자식 객체
  • Map에서는 SortedMap의 자식 객체(key 기준)

Collections.sort()

 

  • Collections의 sort()를 이용한 정렬
    • sort(List<T> list) : 객체가 Comparable을 구현하고 있는 경우 내장 알고리즘을 통해 정렬
/**
     * Sorts the specified list into ascending order, according to the
     * {@linkplain Comparable natural ordering} of its elements.
     * All elements in the list must implement the {@link Comparable}
     * interface.  Furthermore, all elements in the list must be
     * <i>mutually comparable</i> (that is, {@code e1.compareTo(e2)}
     * must not throw a {@code ClassCastException} for any elements
     * {@code e1} and {@code e2} in the list).
     *
     * <p>This sort is guaranteed to be <i>stable</i>:  equal elements will
     * not be reordered as a result of the sort.
     *
     * <p>The specified list must be modifiable, but need not be resizable.
     *
     * @implNote
     * This implementation defers to the {@link List#sort(Comparator)}
     * method using the specified list and a {@code null} comparator.
     *
     * @param  <T> the class of the objects in the list
     * @param  list the list to be sorted.
     * @throws ClassCastException if the list contains elements that are not
     *         <i>mutually comparable</i> (for example, strings and integers).
     * @throws UnsupportedOperationException if the specified list's
     *         list-iterator does not support the {@code set} operation.
     * @throws IllegalArgumentException (optional) if the implementation
     *         detects that the natural ordering of the list elements is
     *         found to violate the {@link Comparable} contract
     * @see List#sort(Comparator)
     */
    @SuppressWarnings("unchecked")
    public static <T extends Comparable<? super T>> void sort(List<T> list) {
        list.sort(null);
    }

Collection 클래스에 오버로딩 되어있는 수많은 sort 중 하나를 가져와보았다.

 

All elements in the list must be <i>mutually * comparable</i> using the specified comparator

 

모든 element 들은 서로 Comparable 해야한다는 뜻인데, 우리가 평소 고민없이 정렬하던 Integer, String 등은 모두 내부적으로 Comparable을 상속하고 있다.

 

내부적으로 Comparable 인터페이스를 상속

 

String도 마찬가지

 

Comparable?

Comparable은 정렬하고자 하는 객체 클래스에 "정렬 기준"을 부여하는 녀석이다.

Comparable 인터페이스를 상속받은 객체들은 compareTo를 강제로 구현해야한다.

compareTo를 통해 다른 객체와 비교를 진행한 후 -1, 0, 1을 리턴하게 된다. (?)

 

뜬금없이 -1, 0, 1을 리턴한다고 느끼겠지만, sort를 하게되면 모든 요소가 compareTo 메소드를 통해 정렬을 하게되는데, 요소 각각 비교할때 compareTo 의 리턴값에 따라 "자리를 바꿀지 말지" 정하게 된다.

 

이해하기 쉽게, 그동안 많이 사용했던 Integer의 "정렬 기준"을 한번 살펴보자.

    /**
     * Compares two {@code Integer} objects numerically.
     *
     * @param   anotherInteger   the {@code Integer} to be compared.
     * @return  the value {@code 0} if this {@code Integer} is
     *          equal to the argument {@code Integer}; a value less than
     *          {@code 0} if this {@code Integer} is numerically less
     *          than the argument {@code Integer}; and a value greater
     *          than {@code 0} if this {@code Integer} is numerically
     *           greater than the argument {@code Integer} (signed
     *           comparison).
     * @since   1.2
     */
    public int compareTo(Integer anotherInteger) {
        return compare(this.value, anotherInteger.value);
    }
    public static int compare(int x, int y) {
        return (x < y) ? -1 : ((x == y) ? 0 : 1);
    }

1. x와 y를 비교한다.

2. 비교 주체(x)의 값이 작으면 -1, 같으면 0, 크면 1을 리턴한다.

2-1)  -1 이라면 비교 주체가 앞으로 온다. 

2-2) 0이라면 변화 X

2-3) 1이라면 비교 주체가 뒤로 간다.

 

이런 식으로 오름차순 정렬이 된다. Integer인  5와 1을 비교하는 예시를 들어보자면 다음과 같다.

  • 5와 1 비교 ( 5 - 1 = 4) 
  • 리턴은 양수, 0, 음수 중 하나로 리턴
  • 5 와 1을 비교할 때 양수 4를 리턴함 → 방향 전환 :  1이 앞으로, 5가 뒤로
  • 만약 5 와 5를 비교하면 0 리턴 → 방향 유지 (같음 - 자리바꿀 필요X)
  • 1 과 5를 비교하면 음수 -4 리턴 → 방향 유지

아래는 String을 기준으로 정렬하는 예시이다.

 public class SmartPhone implements Comparable<SmartPhone> {

			...
			...

// 핸드폰 number를 오름차순 정렬하도록 구현
    @Override
    public int compareTo(SmartPhone o) {
    	return this.number - o.number;
    }

 

/**
	ListSortTest.java
**/

...
...
public void sortPhone() {
        // TODO: 전화 번호에 따라 SmartPhone을 정렬해보자.
    	List<SmartPhone> phones = Arrays.asList(new SmartPhone("010"),
                            			new SmartPhone("011"), 
                                		new SmartPhone("000"));
    	System.out.println("정렬 전 : " + phones);
    	Collections.sort(phones);
    	System.out.println("정렬 후 : " + phones);
        // END
    }

// 출력
// 정렬 전 : [전화 번호: 010, 전화 번호: 011, 전화 번호: 000]
// 정렬 후 : [전화 번호: 000, 전화 번호: 010, 전화 번호: 011]

Comparator

Comparator도 Comparable과 마찬가지로 정렬을 위해 사용되는 인터페이스이다.

Comparator를 통해 객체들을 비교하고 정렬한다. 주로 Comparable 인터페이스를 구현하지 않았거나, 기존의 정렬 순서를 덮어쓸때 사용한다. 

 

Comparable을 구현하지 않았거나?

이게 무슨 말이냐면, Comparable은 클래스 내에서 compareTo를 오버라이딩 하는 방식으로 구현한다. 하지만 클래스에 접근할 수 없는 상황이거나 굳이 구현할 필요 없이 일회성으로 정렬을 해야할 때 Comparator를 사용한다는 의미이다.

 

예시를 통해 더 살펴보자.

public class Person {
    private String name;
    private int age;

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

 

이 Person 클래스를 age 순으로 내림차순 정렬하고 싶지만 Person 클래스에 접근 할 수 없을 때, Comparator를 사용해보자.

import java.util.Comparator;
import java.util.Arrays;

public class Main {

    public class AgeComparator implements Comparator<Person> {
        @Override
        public int compare(Person person1, Person person2) {
            return -1 * (person1.getAge() - person2.getAge());
        }
    }
    public static void main(String[] args) {
        Person[] people = {
            new Person("Alice", 28),
            new Person("Bob", 23),
            new Person("Charlie", 35),
            new Person("David", 20)
        };

        Arrays.sort(people, new NameComparator());

        for (Person person : people) {
            System.out.println(person);
        }
    }
}

자세히 보면 compare은 주체와 비교대상이 나뉘어져있는 compareTo와 다르게, 비교하는 대상들을 파라미터로 받아서 비교하고 있음을 유의하자.

 

compare도 compareTo와 마찬가지로, 비교 메소드의 리턴값이 -1이면 앞으로, 0이면 그대로, 1이면 뒤로 간다.

// 출력값
// Charlie (35)
// Alice (28)
// Bob (23)
// David (20)

 

💡 Comparable VS Comparator?

 

Comparable은 자기 자신과 비교하기에 비교할때마다 CompareTo를 사용

Comparator는 매개변수끼리 비교한다.

 

즉, Comparable은 클래스 자체에 기본적인 정렬기준을 부여하는 녀석

Comparator는 상황마다 그때 그때 정렬을 하고 싶을 때 정렬 기준을 주기 위해 쓴다

 

 

한 번에 이해하기가 힘든 개념이기 때문에 아래 예제를 풀며 익숙해지길 바란다!

 

예제

 

package com.ssafy.corona.virus;

public class Student {
    private static String name;
    private static int age;
    
    private static Student instance = new Student("test", 10);
    
    public static Student getInstance() {
    	if(instance == null) {
    		instance = new Student("test", 10);
    	}
    	return instance;
    }
    
    

    Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
    	
    	return "이름 : " + name + " / 나이 : " + age;
    }
}
package com.ssafy.corona.virus;

import java.util.Arrays;

public class StudentTest {
	public static void main(String[] args) {
		Student[] school = new Student[5];
		school[0] = new Student("James", 10);
		school[1] = new Student("John", 13);
		school[2] = new Student("Alex", 17);
		school[3] = new Student("Grey", 21);
		school[4] = new Student("Max", 8);
		
		// 1. 다음 코드로 school 배열을 정렬해주세요.
		// Student 클래스를 수정하여 이름을 기준으로 사전순으로 정렬해주세요.
		Arrays.sort(school);
		for(Student s: school) {
			System.out.println(s.toString());
		}
		
		
		// 2. Student 클래스를 수정하지 않고 오로지 아래 코드를 수정하여 정렬해주세요.
		// 나이를 기준으로 내림차순 정렬해주세요.
		
//		Arrays.sort(school);
		
		
		
		
		
	}
}