Comparable과 Comparator

hoonie·2023년 2월 22일
0

basic-java

목록 보기
1/1

스스로 공부한 내용을 기록한 포스트로 틀린 부분이 있을 수 있으니 오류를 확인해보시고 적용하시길 강력히 당부드립니다.

https://st-lab.tistory.com/243 를 참고하여 작성한 포스트입니다.

포스트의 시작 지점

알고리즘 문제를 풀다보면 정수들을 정렬해야하는 경우가 있다. primitive type인 int로 선언된 정수들은 Arrays.sort()로, wrapper class인 Integer로 선언된 정수들은 Collections.sort()로 손쉽게 오름차순으로 정렬할 수 있다.
내림차순의 경우 Collections.sort(list, Collections.reverseOrder())로 정렬할 수 있는데 이것은 wrapper class인 경우만 가능하다. 따라서 배열을 내림차순으로 정렬하고 싶은 경우 int[]가 아닌 Integer[]로 선언해주어야 한다.

문제 제기

하지만 사용자가 임의로 선언한 객체를 원하는 기준으로 정렬하고 싶은 경우는 단순히 sort()를 사용할 수 없다. 이런 경우 자바에서 제공하는 Comparable 혹은 Comparator 인터페이스를 활용하여 객체를 정렬할 수 있다. 인터페이스는 특정 메서드의 구현을 강제하는 속성이 있기 때문에 두 인터페이스에서 반드시 정의해줘야하는 메서드가 있다.

이제 ComparableComparator가 무엇인지, 둘의 차이는 어떤게 있는지, 활용은 어떻게 하는지 알아보자.

Comparable과 Comparator

두 인터페이스를 사용하는 이유에 대해서 생각해보면 정렬하기 위해 사용한다고 생각할 것이다. 하지만 이것은 좁은 의미에서 맞는 말이라고 할 수 있다.

두 인터페이스를 사용하는 궁극적인 목표는 두 객체를 '비교'하기 위함이다.

예를 들어 나이와 키가 정보로 주어진 어떤 두 사람이 있다고 하자. 이 두 사람을 비교해 달라는 요청을 받았을 때 반드시 되물어야할 질문이 있다. "나이로 비교할까요? 키로 비교할까요?" , "나이로 비교한다면 나이가 많은 사람을 우선순위에 둘까요? 적은 사람을 우선순위에 둘까요?" 등 정해줘야할 기준이 모호할 것이다. 바로 이런 문제를 해결하기 위해 두 인터페이스가 필요하다.
또한 반드시 구현해줘야하는 메서드의 파라미터도 차이가 있다.
Comparable의 경우 compareTo(T o)메서드에서 파라미터를 1개를 받고 Comparator의 경우 compare(T o1 T o2)메서드에서 파라미터 2개를 받는다. 대략적으로 감이 왔겠지만 compareTo()는 나 자신과 다른 객체를 비교하는 것이고 Compare()은 두 객체를 비교하는 것이다. 이제 기본적인 내용은 알아봤고 각각의 특징에 대해서 알아보자.

Comparable 인터페이스

public class Person implements Comparable<Person>{
        int age;
        int height;

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

        @Override
        public int compareTo(Person o) {
            return this.height - o.height;
        }
}
  • Person이라는 사용자 정의 객체를 만들었다.
  • Comparable인터페이스를 사용하였고 반드시 CompareTo(T o)메서드를 구현해주어야한다.
  • 여기서 리턴 타입이 int임을 확인할 수 있는데 그렇다면 정수값을 가지고 어떻게 비교를 하는 것일까?
    • 자기 자신과 상대방의 값을 비교하여 나타낸다.
    • 즉, 자신의 height가 173이고 상대방의 height가 180이면 내가 7만큼 작은것이니 -7만큼 더 큰 것이고, 상대방의 height가 170이면 내가 +3만큼 더 크다고 할 수 있다.
  • (중요)다양한 방법으로 int값을 리턴할 수 있지만 위와 같이 작성하는 경우 반드시 고려해야하는 부분이 있다.
    • 만약 문제의 조건에 따라 리턴값이 int의 범위를 넘어갈 수도 있는 경우에는 오류가 발생한다. 그런 경우에는 case를 나눠 <, ==, >를 사용하여 리턴값이 int 범위가 되도록 하여야한다.
    • 예를들어 나의 값이 1이고 상대방의 값이 -2,147,483,648 (Integer.MIN_VALUE)인 경우 리턴값이 2,147,483,649가 되므로 오류가 overflow 이슈가 발생한다.

Comparable정리

  1. 나 자신과 상대방을 비교하는 방식
  2. compareTo(T o1)을 반드시 정의해줘야한다.

Comparator 인터페이스

public class Person implements Comparator<Person> {
        int age;
        int height;

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

        @Override
        public int compare(Person o1, Person o2) {
            return o1.height - o2.height;
        }
}
  • Person이라는 사용자 정의 객체를 만들었다.
  • Comparator인터페이스를 사용하였고 반드시 Compare(T o1, T o2)메서드를 구현해주어야한다.
  • 비교하는 방식은 compareTo(T o)과 비슷하지만 compare(T o1, T o2)는 자기자신이 아니라 서로 다른 두 객체를 비교한다는 점에서 차이가 있다. 즉, 비교를 하는 과정에서 자기 자신은 두 객체와 상관이 없다는 뜻이다.
	Person personA = new Person(28, 183);
    Person personB = new Person(25, 177);
    Person personC = new Person(21, 173);

    int isBig = personA.compare(personB, personC);
    if(isBig > 0){
        System.out.println("personB가 personC보다 큽니다.");
    } else if (isBig == 0){
        System.out.println("personB와 personC가 같습니다.");
    } else {
        System.out.println("personB가 personC보다 작습니다.");
    }
    
    // 출력결과
    // personB가 personC보다 큽니다.
  • 위 코드에서 알 수 있듯이 어떤 두 객체를 비교하기 위해선 compare메서드를 호출해야하는데 비교 대상이 아닌 (personA)의 메서드에 파라미터로 personB, personC를 넣는 것이나 비교 대상 중 하나인 personB의 compare메서드를 호출해서 파라미터로 personB, personC를 넣는 것이나 결과는 똑같다.
  • 다시말해 어떤 객체의 compare를 사용하던지 상관이 없다면 비교만을 위한 객체를 생성해서 compare메서드를 사용할 수도 있을 것이다.
    • 예를들어 Person onlyForCompare = new Person(0,0)을 만들고 onlyForCompare.compare(personB, personC)와 같이 사용할 수도 있을 것이다.
    • 하지만 여기서 의문점이 들었을 것이다. 굳이 사용하지도 않는 객체를 만들어서 비교메서드만 사용하는것이 효율적일까?
    • 해결하는 방법은 익명 객체(클래스)를 이용하는 것이다.

익명 객체란?

익명 객체는 무엇일까? 쉽게 말해서 이름이 없는 객체(클래스)라는 것이다. 아래 코드들을 보면서 이해를 해보자.

public class Anonymous {
    public static void main(String[] args) {

        Rectangle a = new Rectangle();

        // 익명객체1
        Rectangle anonymous1 = new Rectangle() {

            @Override
            int get() {
                return height;
            }
        };

        // 익명객체2
        Rectangle anonymous2 = new Rectangle() {
            int depth = 5;

            @Override
            int get() {
                return height * width * depth;
            }
        };

        System.out.println(a.get());
        System.out.println(anonymous1.get());
        System.out.println(anonymous2.get());

    }

    static class Rectangle {
        int width = 20;
        int height = 30;

        int get() {
            return width;
        }
    }
}
  • 일반적으로 객체를 생성하는 방식은 Rectangle a = new Rectangle()과 같은 방식일 것이다.
  • 하지만 익명 객체는 Rectangle anonymous1 = new Rectangle() { //..구현부..//}로 생성한다. 거의 유사하지만 우리는 {} 안의 구현부에 집중해야한다.
  • 일반적으로 우리가 객체를 구현한다고 하면 변수를 선언하고, 메서드를 구현하는 것이다. 이런 방식으로 객체를 구현할때는 반드시 이름을 지정해준다.
  • 하지만 Rectangle anonymous1 = new Rectangle() { //..구현부..// }에서 구현부를 보면 우리가 일반적으로 변수를 선언하고, 메서드를 재정의(override)하는 부분들이 있기 때문에 Rectangle과는 다른 새로운 객체를 생성하였다. 하지만 어느 부분에서도 객체의 이름을 찾아볼 수 없다.
  • 이렇게 클래스 이름이 정의되어 있지 않는 객체를 익명 객체라고 한다.

익명 객체에 대한 설명을 간략하게 하였는데 이해가 잘 되지 않는다면 포스트 맨 위 참고 페이지를 참조하길 바란다.

익명 객체를 활용한 Comparator

익명 객체에 대해 길게 알아본 이유에 대해서 다시 짚고 넘어가야한다. 우리는 두 객체를 비교해주는 Comparator를 구현해서 기능만 사용하고 싶기 때문에 익명 객체를 활용하기로 하였다.
분명히 Comparator는 인터페이스이기 때문에 구현(상속)할 대상이 존재한다. 즉 익명객체로 만들 수 있다는 것이다.
따라서 이름은 정의되지 않지만 Comparator를 구현하는 익명객체를 만들어서 compare 메서드를 사용할 수 있을 것이다. 코드를 통해 구현방법을 알아보자.

import java.util.Comparator;

public class Test {

    public static void main(String[] args) {

        // 익명 객체 구현 1
        Comparator<Person> comp1 = new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.age - o2.age;
            }
        };
    }

    // 익명 객체 구현 2
    public static Comparator<Person> comp2 = new Comparator<Person>() {
        @Override
        public int compare(Person o1, Person o2) {
            return o1.age - o2.age;
        }
    };
}


// 외부에서 익명 객체로 Comparator를 생성하기 때문에 클래스에서 Comparator를 구현할 필요가 없다.
class Person {
    int age;
    int height;

    public Person(int age, int height) {
        this.age = age;
        this.height = height;
    }
}
  • 이렇게 익명객체를 활용하면 객체를 생성할 필요없이 변수명(comp1, comp2)만 바꿔서 자신이 원하는 기준으로 compare를 정의할 수 있다는 장점이 있다.
  • Comparable을 익명객체로 구현할 순 없을까?
    • 구현은 할 수 있겠지만 원하는 대로 동작하기 힘들것이다. 이유는 간단하다. compareTo메서드는 자기 자신과 상대방을 비교하는 것이기 때문에 익명 객체와 상대방을 비교할 수 밖에 없기 때문에 원하는 로직을 수행하기 부적절하다.

Comparable, Comparator와 정렬 관계

이제 Comparable, Comparator 두 인터페이스의 차이는 이해가 되었을 것이다.
객체를 비교하기 위해 compareTo, compare를 사용하는 것은 사용자가 정의한 기준을 토대로 양수, 0, 음수를 중 하나를 반환하는 것이다.

정렬관계를 알아보기 전에 한 가지 알고 가야할 것이 있다. Java에서의 일반적인 정렬기준이다. java는 특별한 정의가 되어 있지 않는 한 오름차순으로 정렬을 한다.
예를 들어 {1, 3, 2} 배열이 있다고 해보자. 우리가 정렬 알고리즘을 사용하면 두 수를 비교하게 된다. 0번 인덱스의 1과 1번 인덱스의 3을 비교하는 과정에서 1 - 3을 하면 음수가 반환될 것이다. java는 오름차순을 기본으로 하기 때문에 compareTo와 compare가 반환하는 값이 음수이면 선행 원소가 후행 원소보다 작다는 뜻이므로 위치를 교환하지 않는다. 그렇다면 1번 인덱스의 3과 2번 인덱스의 2를 비교해보자. 3 - 2를 하면 양수가 반환되고 이 뜻은 선행원소가 후행원소보다 크다는 뜻이므로 위치를 교환한다. 정리를 하면 오름차순을 디폴트로 두는 java의 특성상 두 수를 비교할 때 반환되는 값이

  • 음수인 경우: 위치를 교환하지 않는다.
  • 양수인 경우: 위치를 교환한다.

이를 염두해두고 객체를 내림차순으로 정렬하고자 하면 어떻게 해야할까?
앞서 비교하는 메서드의 반환값이 음수이면 위치를 교환하고, 양수이면 위치를 교환하지 않는다고 하였다. 그렇다면 선행원소가 후행원소보다 클 때 반환되는 값이 양수가 되도록 하면 java는 위치를 교환하지 않을 것이다. 따라서 리턴값을 반대로 하면 원하는 대로 내림차순으로 정렬될 것이다. 쉽게 말해 (선행원소 - 후행원소)의 값이 음수일 때 반대로 바꿔주어 양수가 반환되도록하면 내림차순 정렬이 완성될 것이다. 즉, 우리가 사용했던 Person예제에서 compare(T o1, T o2)의 반환부인 return o1.height - o2.heightreturn -(o1.height - o2.height)로 바꿔주기만 하면 된다. 이를 좀 더 간단히 하면 return o2.height - o1.height으로 바꿔줄 수 있다.

profile
사우루스 팡팡!

0개의 댓글