본문 바로가기
질문을 해결하는 과정

[Java] Comaprable, Comparator를 샅샅히 파헤쳐보기

by 무자비한 낭만주먹 2024. 1. 3.

[그림1]. 오늘도 감사한 공부 시작 ~

 

목차
0. 개요
1. Comparable 인터페이스를 상속 받아야 하는 이유
2. 사실 이미 Comparable을 상속받는 객체 타입들
3. 그려 ~? 그럼 Collections.sort()는 어뗘?
4. Comparator가 뭐여 ..?
5. 함수형 인터페이스 ..? 그럼 람다?
6. 마치며

 

 

 

0. 개요
최근 알고리즘 풀이중에 정렬 문제 풀이에 "객체 정렬"이라는 방법을 이용하면 쉽게 풀 수 있다는 사실을 알았다. 예를 들어 아래 문제를 보자.

 

Q.  3명의 학생들의 수학, 영어, 국어 성적이 각각 주어졌을 때 수학 성적 순으로 정렬 후 출력해라.

// Method1. Comparable을 이용한 방법
..
class Student implements Comparable<Student>{
    private final int math;
    private final int english;
    private final int korean;

    @Override
    public comapreTo(Student other){
        return this.math - other.math;
    }
    ..
}

class Main{
    pirvate static final int n = 3;
    public static void main(String[] hyeok){
        ..
        Student[] students = new Student[];
        for (int i = 0; i < n; i ++{
            StringTokenizer st = new StringTokenizer(br.readLine());
            int math = Integer.parseInt(st.nextToken);
            int kor = Integer.parseInt(st.nextToken);
            int eng = Integer.parseInt(st.nextToken);

            students[i] = new Student(math, kor, eng);
        }
        Arrays.sort(students);

        for (Student student: students){
            bw.write(student.scoreAbout());
        }
    }        
}

[그림2]. 오잉 ..?

그냥 Comparable 인터페이스 상속받고 compareTo 메소드를 재정의한 거 밖에 없는데 Array 클래스의 sort에 인자로 넘겨주니까 알아서 정렬을 해버렸다. 그냥 딱 봤을 땐 "아 .. Arrays 클래스의 sort 메서드가 저 Comparable를 상속받는 클래스에서 재정의하는 compareTo 메서드를 이용해서 뭘 하나보다 ~" 정도로 생각을 마무리 했는데 계속 궁금해서 이 참에 한번 알아보기로 했다. 유사한 방식 중에 Comparator라고 하는 함수형 인터페이스 활용하는 방법도 있는데 그 부분도 다룰 예정이다.

 

1. Comparable 인터페이스를 상속 받아야 하는 이유
지피지기면 백전무패라는 말을 아는가? 일단 왜 그런지 파악하기 위해 Comparable 인터페이스를 까보자.

 

[그림3]. Comparable 인터페이스

...

 

[그림4]. 음 ..

일단 침착하고 내가 원하는 설명만 찾아보자

[그림5]. Comparable 인터페이스가 사용되는 상황

위 내용을 통해 "이 인터페이스를 상속받는 클래스를 요소로 하는 배열이 Arrays.sort나 Collection.sort에 인자로 전달되면 상속받은 클래스가 재정의한 compareTo 기준으로 알아서 정렬을 해준다는 내용이구나 ~"라는걸 이해했고 Collections와 Arrays를 찾아가보기로 했다.

[그림6]. Arrays의 sort 메서드

대략 Arrays의 sort 메서드는 병합정렬을 쓰는구나, 객체 배열에 대해 정렬하기 sort 메서드에 인자로 넘어온 배열의 요소들은 무조건 Comparable을 상속 받아야하는구나 .. 같은 얘기들이었다. 위에 LegacyMergeSort는 구 버전의 sort와 호환하기 위해 남겨둔 거여서 그냥 넘어가고 그 아래 ComparableTimSort 쪽으로 더 들어가보자.

[그림7]. ComparableTimSort의 sort 메서드와 요 놈이 사용하는 binarySort 메서드

드디어 Comparable을 사용하는 곳을 찾았다. 일단 들어온 순서를 먼저 복기해보면
[Arrays.sort()] -> [ComparableTimSort.sort()] -> [ComparableTimSort.binarySort()]이다. 결국 저 안에서 비교하는데 Comparable의 compareTo가 필요하기에 객체 정렬을 하고자 하는 클래스는 무조건 Comparable을 상속받아 compareTo를 재정의해야 했던 것이다.

 

2. 사실 이미 Comparable을 상속받는 객체 타입들

[그림8]. 아니 근데 잠깐만
[그림9]. Integer도 Compable 상속 받고 compareTo() 재정의 되어있잖아?

사실 Integer, String 같은 참조 타입들은 모두 Compable을 상속받고 각자 compareTo()를 재정의하고 있었다. 결국 모두 Arrays.sort가 가능했던 것이다. 왜 그런진 모르겠지만 Integer이 뭔가 Collections랑 찐~한 사이인 거 같은 느낌이어서 Integer 관련된 건 죄다 Collections.sort로 하는 줄 알았는데 공부하다보니 이게 Arrays랑 Collections는 데이터들의 공통된 "타입"으로 분류하는게 아니라 그걸 묶는 "방식"으로 분류를 하는 거였다. 그니까 다시 정리하면
# Integer, int => Arrays랑 Collections랑 아무 상관 없음.
    [배열 정렬]
    - Integer[] numberArray는 `Arrays.sort(numberArray)`로 정렬
    - int[] numberArray는 `Arrays.sort(numberArray)`로 정렬

    [Collection정렬]
    - List<int> numberList => error, (단순히 primitive Type이 List에 안담길 뿐이다.)
    - List<Integer> numberList는 `Collcetions.sort(numberList)`로 정렬

 

3. 그려 ~? 그럼 Collections.sort()는 어뗘?

[그림10]. 다 똑같혀 ~
[그림11]. 사실 list.sort는 결국 Arrays.sort()를 이용해 똑같이 Timsort를 한다. (Comparator를 사용하는게 다르긴 하지만)

Collections를 sort할 때도 Comprable을 상속받는 객체에 대해서 내부적으로 Arrays 클래스의 sort()를 돌린다. 다만 Comparator라는게 추가되긴 하는데 이게 뭔지는 뒤에 설명하고 결론은 Collections를 이용한 sort나 Arrays를 이용한 sort나 TeamSort를 사용하는데 그 구현은 ComparableTeamSort와 TeamSort에 구현되어있고 TeamSort의 sort는 Comparator라는 걸 사용한다.

 

4. Comparator가 뭐여 ..?

얘도 Comparable 같은 함수형 인터페이스다. Comparable과의 차이는 기존에 원하는 객체 자체에 상속시켜 사용하던 Comparable과 다르게 Collections의 sort 메서드의 인자로 익명 클래스로 재정의해서 넘길 수 있어 클라이언트 관점에서 좀 더 유연하게 정렬 방식을 변경할 수 있다는 점이다. 사용법을 한 번 보자

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.Comparator;
import java.util.Arrays;

class Student{
    private final String name;
    private final int height;
    private final double weight;

    public Student(String name, int height, double weight){
        this.name = name;
        this.height = height;
        this.weight = weight;
    }
}


public class Main {
    public static void main(String[] args) throws IOException{
        ..
        // TODO2. COMPARATOR 이용해 바로 정렬
        Arrays.sort(students, new Comparator<Student>() {
            @Override
            public int compare(Student a, Student b){
                return a.name.compareTo(b.name);
            }
        });
        
        ..
        Arrays.sort(students, new Comparator<Student>(){
            @Override
            public int compare(Student a, Student b){
                return b.height - a.height;
            }
        });
        ..
    }
}

 

기존에 Comparable 처럼 클래스 작성시에 상속후 compareTo 메서드를 재정의해 정렬 방법을 정의할 수도 있지만 comparator 인터페이스 처럼 익명 클래스를 생성해서 넘겨줌으로써 클라이언트 입장에서 좀 더 유연하게 객체정렬을 사용할 수 있도록 해주는 방법도 있다는 걸 알 수 있다. 그래서 결론은 객체 정렬을 좀 더 편리하게 해주기 위해 Collections에 인자로 넘길 클래스 정의에 필요한 함수형 인터페이스이다.

 

5. 함수형 인터페이스 ..? 그럼 람다?

함수형 인터페이스 하면 딱 람다가 떠오른다. 자세한 내용은 지금 다루기 어렵고 결론만 딱 놓고 말하면

/* 기존 익명 클래스를 이용한 방식 */
Arrays.sort(students, new Comparator<Student>(){
    @Override
    	public int compare(Student a, Student b){
        	return b.height - a.height;
        }
    }
);
/* 람다를 이용한 개선 */
Arrays.sort(students, (a, b) -> b.height - a.height);

 

6. 마치며

[그림12]. Comparable과 Comparator 그리고 Arrays, collections sort의 관계

 

뭔가 쓰다보니 횡설수설이 된 거 같기도 하지만 어쨌든 Comparable과 Comparator는 객체 정렬을 지원하기 위해 만들어진 함수형 인터페이스고 클래스에 직접 상속시켜 비교를 위한 메서드를 재정의하냐, 아니면 Collections의 sort의 인자로 익명 클래스를 만들어 전달하냐의 사용 방법의 차이만 있을 뿐 내부적으로는 같은 객체 정렬 알고리즘이다.

 

 

오늘 하루도 공부할 수 있어 크게 감사합니다

2023-12-10 개발자 최찬혁