[Effective Java] item14 - Comparable을 구현할지 고려하라

신민철·2023년 4월 19일
1

Effective Java

목록 보기
14/23
post-thumbnail

코딩을 하면서 값을 비교하는 경우는 굉장히 많다! 다양한 경우가 많은데 새로운 객체를 만들 때는 Comparable을 구현할지 고려해야 할 것이다.

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

Comparable 인터페이스는 다음과 같이 생겼다.


equals의 규약과 compareTo의 규약은 비슷하다.

  • 객체의 순서를 비교하는데 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다.
  • 비교할 수 없는 객체가 주어지면 ClassCastException을 던진다.
  • Comparable을 구현한 클래스는 모든 x, y에 대하여 sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) 여야 한다.
  • Comparable을 구현한 클래스는 추이성을 보장해야 한다. 즉, x.compareTo(y) > 0, y.compareTo(z) > 0, x.compareTo(z) > 0 이다.
  • Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z)) 다.
  • 필수는 아니지만 (x.compareTo(y) == 0) == (x.equals(y))를 지키는게 좋다.

sgn은 부호 함수이고 음수면 -1, 0이면 0, 양수면 1을 반환한다.

compareTo 규약을 지키지 못하면 haseCode 규약처럼 해시를 사용하는 클래스와 어울리지 못하듯, 비교를 활용하는 컬렉션인 TreeSet이나 TreeMap, 검색과 정렬 알고리즘을 활용하는 Arrays와 어울리지 못한다.

위 규약에서 equals와 같이 반사성, 대칭성, 추이성을 충족해야 한다는 것을 볼 수 있다. 그래서 equals와 같이 기존 클래스를 확장한 구체 클래스에 새로운 값 컴포넌트를 넣게 된다면 compareTo를 지킬 방법이 없다.

그래서 우리는 이전에 equals에서 원래 클래스를 가리키는 내부 인스턴스를 두고 ‘뷰’ 메소드를 제공하였다. compareTo도 마찬가지로 적용할 수 있다.

마지막 규약은 필수는 아니지만 꼭 지키길 권장된다. compareTo로 줄지은 순서와 equals의 결과가 일관되게 한다. 일관되지 않으면 이 클래스의 객체를 정렬된 컬렉션에 넣게되면 (ex. Collection, Set, Map) 정의된 동작과는 다르게 흘러갈 것이다.

해당 인터페이스들은 동치성을 비교할 때 equals를 사용하지 않고 놀랍게도 compareTo를 사용한다.

또한 compareTo는 제네릭 인터페이스임에 주의하자. 즉 인수 타입은 컴파일 타임에 결정된다. → 형변환이 필요없다.

equals와 또 비슷하게 중요한 필드부터 비교를 하는 것이 핵심이다.


public class Comp implements Comparable<Comp>{
	String name;
	int age;
	int number;

	@Override
	public int compareTo(Comp o) {
		int result = this.name.compareTo(o.name);
		if (result == 0) {
			result = Integer.compare(this.number, o.number);
			if (result == 0) {
				result = Integer.compare(this.age, o.age);
			}
		}
		return result;
	}
}

여기서는 이름을 먼저 비교하고 전화번호, 그리고 나이를 비교하였다.

또 지켜볼 포인트가 있는데 전에 정수 기본 타입을 비교할 때 >나 <로 비교하였는데 자바 7부터는 상황이 바뀌었다. 박싱된 기본 타입 클래스의 compare 메소드를 사용하면 되는 것이다. 그리고 >나 <는 예상치 못한 오류를 범할 수 있다.


public class Comp implements Comparable<Comp>{
	String name;
	int age;
	int number;

	private final Comparator<Comp> COMPARATOR =
			comparing((Comp c) -> c.name)
					.thenComparingInt(c -> c.number)
					.thenComparingInt(c -> c.age);
	
	@Override
	public int compareTo(Comp o) {
		return COMPARATOR.compare(this, o);
	}
}

위의 비교자 생성 메소드와 메소드 연쇄 방식으로 깔끔하게 코드를 작성할 수 있다. 그런데 약간의 성능 저하가 있을 수 있다.

그럼 여기서 Comparable과 Comparator의 차이가 뭘까? Comparable은 생각해보면 compareTo를 상속받아 사용한다. 규칙을 하나 정하는 수단으로 사용되는 것이다.

반면에 Comparator는 여러 개의 규칙을 사용할 수 있다. 일반적으로 Array나 Collections의 sort()의 경우에 compareTo의 정의된 정렬 기준을 사용하지만 Comparator를 두번째 인자로 넘겨줄 경우에는 해당 기준으로 정렬된다! comparing과 thenComparing은 각각 2개, 3개 다중정의가 되어 있다. 인자가 하나일 때는 자연적인 순서로, 두번째는 추출자와 추출된 키를 비교할 비교자를 받아서 사용한다.



핵심 정리
순서를 고려해야 하는 클래스라면 Comparable 구현을 고려해보자. 여기서 compareTo와 equals의 결과를 동일하게 하는 것을 강력히 권장한다. 또한 정수를 비교할 때 compare 메소드나 Comparator 비교자 생성 메소드를 사용하자.

0개의 댓글