Comparable

공병주(Chris)·2022년 3월 5일
1

Java를 탄탄히

목록 보기
4/9

이런 저런 프로그램을 만들다보면, 객체들의 순서를 정해야 하는 상황이 있다.
순서를 정하는 방법에 이런 저런 방법이 있겠지만, Comparable을 통한 방법이 가장 기본이라고 생각한다.

Comparable의 개념

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

Comparable은 interface로 compareTo 메소드 하나만을 가진다.

이펙티브 자바에서 설명하는 Comparable의 개념을 살펴보면

  • Comparable을 구현(implements)한 객체는 자연적인 순서를 가진다.
  • 자바 플랫폼 라이브러리의 거의 모든 값 클래스와 열거 타입은 Comparable이 구현되어 있다.
  • equals와 같이 동등성의 개념도 가진다.
    ❗️(그렇다고 a.compareTo(b) == 0 이면 a.equals(b) == true 라는 말은 아니다)❗️
    그렇지만 위의 명제가 만족하도록 구현하는 것을 권고한다.

Comparable의 일반 규약

순서라는 개념에 초점을 맞춰 머릿속으로 생각해보면 쉽게 이해할 수 있는 규약들이다.

  • x.compareTo(x) == 0
    → 자기 자신과 키를 비교하면 같아야 한다(0).
  • sgn(x.compareTo(y) == -sgn(y.compareTo(x))
    → A는 B보다 키가 크면, B는 A보다 키가 작다.
  • x.compareTo(y) > 0이고
    y.compareTo(z) > 0 이면
    x.compareTo(z) > 0 이다.
    → A는 B보다 키가 크고, B가 C보다 키가 크면, A는 C보다 키가 크다.
  • x.compareTo(y) == 0 이면
    x.compareTo(z) == y.compareTo(z)이다.
    → A랑 B가 키가 같고, B가 C보다 키가 크면 A도 C보다 키가 크다.
  • ❗️ 일반 규약은 아니지만 지키면 좋은 것❗️
    x.compareTo(y)가 0이라면 x.eqauls(y) == true

동작방식

기본적으로 반환되는 int 값을 통해 순서를 메긴다.
주어진 객체와 자신을 비교해서 자신이 주어진 객체보다
자신을 주어진 객체보다 앞 순서로 정렬시키려면 양수
자신을 주어진 객체보다 뒤 순서로 정렬시키려면 음수
자신과 주어진 객체의 순서가 같다면 0을 반환하도록 하면 된다.

사용 방법은 아래와 같이 논리적인 순서를 필요로 하는 객체가 Comparable의 compareTo 메소드를 구현하도록 하면 된다.
Comparable의 제네릭에는 비교하고자 하는 객체의 타입을 작성하면 된다. 보통 같은 객체 간의 순서를 정하기 때문에, Comparable을 구현하는 객체와 같은 타입을 넣으면 된다.

예시)

public class Car implements Comparable<Car> {
	private final CarName carName;
    private final int distance;

	@Override
    public int compareTo(final Car another) {
		// 오름차순 정렬하기
        return this.distance - another.distance;
    }
}

위와 같이 compareTo를 구현한다면 another의 distance가 this의 distance보다 크다면 this가 앞 순서로 가기 때문에 distance가 작은 값부터 앞 순서로 정렬할 수 있다.

주의점

public class Car implements Comparable<Car> {
	private final CarName carName;
    private final Distance distance;

	@Override
    public int compareTo(final Car another) {
		//오름차순 정렬하기
        return this.distance.compareTo(another.distance);
    }
}

위와 다르게 비교에 필요한 값이 원시 값이 아닌 객체라면 아래와 같이
비교에 필요한 객체도 Comparable의 compareTo를 구현해주어야한다.
당연한 것이다. 순서를 메기기 위한 값에 순서가 없다면 말짱 도루묵이다.

public class Distance implements Comparable<Distance> {
    private int distance;
	
    @Override
    public int compareTo(final Distance another) {
    	//오름차순 정렬하기
        return this.distance - another.distance;
    }
}

아래와 같은 코드는 위의 코드보다 덜 객체 지향적이기 때문에 피하는 것이 좋다.
값을 꺼내서 비교하기 보다는 Distance에게 메시지를 던지자.

public class Car implements Comparable<Car> {
	private final CarName carName;
    private final Distance distance;

	@Override
    public int compareTo(final Car another) {
		//오름차순 정렬하기
        return this.distance.getValue() - another.distance.getValue();
    }
}

추가적으로

  • Comparable은 제네릭의 특성이 있기 때문에, compareTo 메소드에서 타입 체크를 해주지 않아도
    제네릭에 의해 컴파일 시점에 오류가 발생한다.
  • compareTo의 매개변수로 null 값이 들어오는 경우
    어차피 null 값의 필드에 접근을 하려는 순간 NPE가 발생할 것이지만, NPE가 발생하는 것을 원하지 않는다면 처리를 해주는 것이 좋다.

문제점 및 해결 방안

자동차의 정렬 기준이 오름차순에서 내림차순으로 변경된다면 도메인이 변경에 일어난다.

도메인에서 한가지 방법으로 고정해두고 외부에서 조정하면 된다.

Comparator를 통한 방법

public class CarComparator implements Comparator<Car> {
    @Override
    public int compare(Car firstCar, Car secondCar) {
        return secondCar.compareTo(firstCar);
    }
}
return cars.stream()
        .sorted(new CarComparator()) // 정렬 기준을 지정
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException());

Stream의 sorted()의 인자Comparator 생성하며 구현해서 주입

return cars.stream()
        .sorted(new Comparator<Car>() {
            @Override
            public int compare(Car firstCar, Car secondCar) {
                return secondCar.compareTo(firstCar)
            }
        })
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException());

//람다식으로 변경
return cars.stream()
        .sorted((firstCar, secondCar) -> secondCar.compareTo(firstCar))
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException());

Stream의 sorted()의 인자에 Comparator의 메소드 사용

return cars.stream()
        .sorted(Comparator.naturalOrder()) //오름차순
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException());

return cars.stream()
        .sorted(Comparator.reverseOrder()) //내림차순
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException());

비교할 기준이 여러 개인 경우

public class Person implements Comparable<Person> {
    private static final Comparator<Person> PERSON_COMPARATOR =
            Comparator.comparing((Person another) -> another.age)
                    .thenComparing((Person another) -> another.name);
    private final int age;
    private final String name;

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

    @Override
    public int compareTo(final Person another) {
        if (this.age > another.age) {
            return 1;
        } else if (this.age < another.age) {
            return -1;
        }
        return this.name.compareTo(another.name);
    }
}

위처럼 compareTo에 복잡한 로직을 적기 보단 아래 처럼 Comparator의 comparing과 thenComparing 체이닝을 사용할 수 있다.

public class Person implements Comparable<Person> {
    private static final Comparator<Person> PERSON_COMPARATOR =
            Comparator.comparing((Person another) -> another.age)
                    .thenComparing((Person another) -> another.name);
    private final int age;
    private final String name;

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

    @Override
    public int compareTo(final Person another) {
        return PERSON_COMPARATOR.compare(this, another);
    }
}

순서를 매길 것이면 값을 꺼내서 비교해 정렬 시키는 것보다 객체에게 메시지를 보내는 형식이 훨씬 객체지향적인 방식이라고 생각한다.

0개의 댓글