[Java] Comparable, Comparator 인터페이스를 이용한 객체 정렬

yujamint·2022년 7월 25일
0

Java

목록 보기
6/6

알고리즘 문제를 풀다보면 객체를 정렬해야 수월하게 풀 수 있는 문제를 꽤나 마주하게 된다.

int a = 1;
int b = 2;

System.out.println(a > b); // false
System.out.println(a == b); // false
System.out.println(a < b); // true

위 코드에서 확인할 수 있듯이 Java에서의 기본 자료형은 부등호를 통해 쉽게 비교가 가능하다.

그렇다면, 이름과 나이 정보를 갖고 있는 Person 클래스가 있다고 가정하자. 이러한 클래스의 경우 부등호를 통해 대소를 비교할 수 있을까? 그렇지 않다.

이러한 클래스는 이름의 대소를 비교할지, 나이의 대소를 비교할지 즉, 비교 기준이 정해져 있지 않기 때문이다.

우리는 Comparable 또는 Comparator 인터페이스를 이용해서 비교 기준을 정하고, 객체를 비교할 수 있게 만들 수 있다.

두 인터페이스 모두 객체를 비교할 수 있게 만들어준다는 공통점을 가지고 있다.

그렇다면 두 인터페이스의 차이점은 무엇일까?
그것은 바로 '비교대상'이다. 이어서 두 인터페이스에 대해서 각각 설명하겠지만, 그에 앞서 차이점을 먼저 짚고 가자면 다음과 같다.

Comparable : 자기 자신과 매개변수 객체를 비교한다.
Comparator : 두 매개변수 객체를 비교한다.

이러한 차이점을 가진다는 것을 알아두고, Comparable 인터페이스에 대해 먼저 알아보도록 하겠다.

Comparable


위에서 말했듯이, Comparable은 인터페이스이다. 그러므로 Comparable을 이용해서 객체를 정렬하기 위해서는 비교하고자 하는 객체에서 아래 코드와 같이 Comparable 인터페이스를 구현해야 된다.

class Person implements Comparable<Person>{
    String name; // 이름
    int age; // 나이

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

    @Override
    public int compareTo(Person o) {
        /*
        비교 구현
        */
    }
}

이름과 나이 정보를 담고 있는 Person 클래스이다.
비교 기준이 정해져 있지 않은 Person 객체을 비교할 수 있도록 하기 위해 Comparable을 implements 한 것을 확인할 수 있다.

이때, Comparable 인터페이스의 인자로는 비교할 객체의 타입인 Person을 넘겨준다.

이제 필수 구현 부분인 compareTo(o) 메소드를 정의해야 된다. compareTo(o) 메소드를 살펴보면 파라미터로 하나의 객체만 받는 것을 알 수 있다. 두 객체를 비교하는 메소드인데 파라미터가 하나인 이유는 바로 compareTo(o) 메소드를 호출하는 객체가 하나의 비교 대상이기 때문이다. 즉, 자기자신과 비교하는 것이다.

Person p1 = new Person("Choi", 25);
Person p2 = new Person("Yu", 24);

p1.compareTo(p2);

코드를 보고 이해해보자. p1과 p2라는 Person 객체가 있다.
p1이 p2를 인자로 주는 compareTo(o) 메소드를 호출했다.
이는 메소드를 호출한 p1과, 파라미터로 입력된 p2를 비교하는 것이다.

아직 비교 기준은 정하지 않았지만, 비교대상이 p1과 p2라는 것만 알고 넘어가자.

이제 compareTo() 메소드를 구현함으로써 비교기준을 정해보자.

class Person implements Comparable<Person>{
    String name; // 이름
    int age; // 나이

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

    @Override
    public int compareTo(Person o) {
        if(this.age > o.age) {
            return 1;
        }
        else if(this.age == o.age) {
            return 0;
        }
        else { // this.age < o.age
            return -1;
        }
    }
}

Person 객체의 나이를 기준으로 비교할 수 있도록 compareTo(o) 메소드를 구현했다. 메소드를 호출한 객체가 두 비교대상 중 하나이기 때문에 this와 파라미터로 입력받은 o를 비교하는 것이다.

  • this의 나이가 o의 나이보다 크다면 1(양수) return
  • this의 나이가 o의 나이와 같다면 0 reutrn
  • this의 나이가 o의 나이보다 적다면 -1(음수) return

메소드를 실행한 결과는 다음과 같다.

Person p1 = new Person("Choi", 25);
Person p2 = new Person("Yu", 24);

p1.compareTo(p2); // 25 > 24 => 1 return

여기서 Comparable을 이용한 객체 정렬의 핵심은 compareTo() 메소드의 반환값이다.
정확히는 반환값의 부호에 달려있다. ( 양수(0 포함) / 음수 )

compareTo(o) 메소드를 통해 age가 23, 25, 24인 세 개의 객체를 정렬한다고 해보자.(age 기준 오름차순 정렬)
편의상 객체 대신 숫자를 바로 입력한다.

  • 23.compareTo(25)
    - 23이 25보다 작기 때문에 위치를 바꾸지 않는다.
    - 메소드 반환값 : 23 < 25 => -1 return
    - 결과 : 23 25 24
  • 25.compareTo(24)
    - 25가 24보다 크기 때문에 위치를 바꾼다.
    - 메소드 반환값 : 25 > 24 => 1 return
    - 결과 : 23 24 25

위 결과를 보면, 반환값이 1일 때, 위치를 바꾸고 -1일 때는 위치를 바꾸지 않는다는 것을 알 수 있다. 이 점을 이용해서 정렬을 하는 것이다.

그리고 여기서 중요한 것은 -1이냐 1이냐가 아니다.
바로, 양수냐 음수냐가 중요한 것이다.

두 객체의 한 정보를 두고 비교했을 때 compareTo(o) 메소드의 반환값으로 양수가 나온다면 자리를 바꾸고, 음수가 나온다면 자리를 바꾸지 않는 것이다.

이러한 점을 이용해서 compareTo(o) 메소드를 다음과 같이 간단하게 구현할 수 있다.

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

이 코드는 위에서의 구현과 마찬가지로, age를 기준으로

  • this가 o보다 더 크면 양수값 반환
  • this와 0이 같다면 0 반환
  • this가 0보다 작다면 음수값 반환

하지만, 이렇게 메소드를 구현하게 된다면 'Underflow'와 'Overflow'로 인해 예외의 상황이 발생할 수 있음에 주의해야 된다. 이와 관련된 내용은 이 블로그에 자세히 작성돼있다.

Comparator


comapreTo(o) 메소드가 하나의 파라미터를 받는다는 것에서 알 수 있듯이, Comparable 인터페이스의 특징은 자기 자신과 매개변수 객체를 비교한다는 것이었다.

그리고, Comparator 인터페이스는 두 매개변수 객체를 비교한다는 특징을 가진다.

Comparator 인터페이스를 구현 예시를 먼저 보도록 하겠다.

import java.util.Comparator;

class Person implements Comparator<Person> {
    String name; // 이름
    int age; // 나이

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

    @Override
    public int compare(Person o1, Person o2) {
        return o1.age - o2.age;
    }
}

public class test {
    public static void main(String[] args) throws CloneNotSupportedException{
        Person p1 = new Person("Choi", 25);
        Person p2 = new Person("Yu", 24);
        Person p3 = new Person("Kim", 23);

        p1.compare(p2,p3); // 실행결과 : 1(양수)
    }
}

먼저, Comparator 인터페이스는 util 패키지에 있기 때문에 import가 필요하다.

  • Comparable 인터페이스는 lang 패키지에 있기 때문에 import 필요 X

Comparable 인터페이스와의 가장 큰 차이점은 역시나 비교 기준을 세우는 메소드인 compare(o1, o2)이다.

파라미터로 2개의 객체를 받고, 그 두 객체를 비교한다.
그렇다면, compare(o1, o2) 메소드를 호출하는 객체는 비교에 어떤 영향을 미칠까?

아무런 영향도 미치지 않는다.
즉, 위 코드에서의 p1은 p2와 p3의 비교에 아무런 영향도 미치지 않는다는 것이다.
이는 호출한 객체와 매개변수를 비교하는 compareTo(o) 메소드와 다른 점이다.

비교에 영향을 끼치지 않는다면 왜 필요한가?

비교하기 위해서 compare 메소드를 호출하는 객체는 오직 비교만 하면 되는데 이름과 나이 같은 불필요한 정보를 담고 있다는 단점이 있다.

이러한 단점을 보완하기 위해서 익명 객체(클래스)를 활용할 수 있다.

간단하게 요약하자면 익명 객체는 compare를 호출하기 위한, 즉, 비교만을 위한 객체이다.

이 객체를 만들 때마다 compare 메소드를 구현할 수 있고, 그렇기 때문에 비교 기준 제각각 다르게 갖고 있는 익명 객체를 생성할 수 있다는 장점이 있다.

자세한 내용은 References의 블로그를 참고하면 된다.

Conclusion


Comparable은 비교하고자 하는 가장 기본적인 설정(오름차순,내림차순)으로 구현하는 경우가 많고, Comparator은 여러 비교 기준을 생성할 수 있다보니 특별한 정렬을 원할 때 쓸 수 있다.

References


https://st-lab.tistory.com/243

profile
개발 기록

0개의 댓글