Home Item 14 - Comparable을 고려하라
Post
Cancel

Item 14 - Comparable을 고려하라

알파벳, 숫자, 연대, 번호 등 순서가 있는 값 클래스를 작성하면 반드시 Comparable 를 구현하자.

  • Comparable 인터페이스는 compareTo() 라는 하나의 메서드를 정의한다.
  • 이 메서드는 Object 메서드가 아니다.
  • 이 메서드의 성격은 Object의 equals와 유사하지만 아래와 같은 차이가 있음
    • 동치성 비교 뿐 아니라, 순서까지 비교가 가능
    • 제네릭하다

Comparable을 구현했다는 것은 순서가 있음을 뜻함

  • 자바에서 제공하는 모든 값 클래스와 열거 타입이 Comparable을 구현했다.
  • 수많은 제네릭 알고리즘과 컬렉션의 힘을 누릴 수 있음.

compareTo 규약

compareTo 메서드의 일반 규약은 equals과 비슷하다.

Comparable 을 구현한 객체는 다음 규약들을 지켜야 한다.

특징

해당 객체와 주어진(매개변수) 객체의 순서를 비교한다.

  • 해당 객체가 더 크다면 양수를 반환
  • 해당 객체가 더 작다면 음수를 반환
  • 해당객체와 주어진 객체가 같을경우 0을 반환
  • 해당 객체와 비교할 수 없는 타입의 객체가 전달되면 ClassCastException이 발생

대칭성 보장

  • 모든 x, y클래스에 대해서 sgn(x.compareTo(y) == -sgn(y.compareTo(x)) 이다.
  • x.compareTo(y)y.compareTo(x)가 예외를 던질 때에 한해서 예외가 발생해야 한다.
1
2
3
4
5
BigDecimal n1 = BigDecimal.valueOf(23134134);
BigDecimal n2 = BigDecimal.valueOf(11231230);

System.out.println(n1.compareTo(n2));
System.out.println(n2.compareTo(n1));

추이성 보장

객체 x, y, z가 있다고 할 때

  • x.compareTo(y)가 양수이고 y.compareTo(z)도 양수라면, x.compareTo(z)도 양수여야한다.
  • (x > y && y > z 이면 x > z여야 한다.)
  • x.compareTo(y) == 0 일 때 sgn(x.compareTo(z)) == sgn(y.compareTo(z))이어야 한다.

반사성 보장 (equlas 규약과 동일)

  • null이 아닌 모든 참조 값(n1)에 대해 아래 n1.compareTo(n1)은 true
1
2
3
BigDecimal n1 = BigDecimal.valueOf(23134134);

System.out.println(n1.compareTo(n1));

동치성 결과가 equals와 같아야 한다.

  • x.compareTo(y) == 0 일 때 x.equals(y)어야 한다.
1
2
3
4
BigDecimal oneZero = new BigDecimal("1.0");
BigDecimal oneZeroZero = new BigDecimal("1.00");
System.out.println(oneZero.compareTo(oneZeroZero)); // Tree, TreeMap
System.out.println(oneZero.equals(oneZeroZero)); // 순서가 없는 콜렉션
1
2
0
false

같지 않다면 문서화하는 것을 권장한다.

image

equals 규약과의 차이점 정리

  • 대칭성, 추이성, 반사성 규약은 equals와 규약들이 비슷하다.
  • 하지만, 모든 객체에 대해 전역 동치관계를 부여하는 equals와 다르게 compareTo는 타입이 다른 객체를 신경쓰지 않아도 된다.
  • 다를 경우 ClassCastException을 던지면 된다.

작성 요령

  • equals와 비슷하지만, Comparable은 타입을 인수로 받는 제네릭 인터페이스라서 compareTo 메서드의 인수 타입은 컴파일 타임에 정해진다.
  • 입력 인수의 타입을 확인 & 형변환 할 필요가 없다는 의미이다.

  • compareTo 메서드는 각 필드의 동치관계를 보는게 아니라 그 순서를 비교한다.
  • 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출한다.
  • Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 할 경우 Comparator를 쓰면 된다.
  • Sample Code

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      @Getter
      public final class PhoneNumber implements Comparable<PhoneNumber> {
          private short areaCode, prefix, lineNum;
        
      	// 기본 타입 필드가 여럿일 때의 비교자 (91page)
          @Override
          public int compareTo(PhoneNumber pn) {
              int result = Short.compare(areaCode, pn.areaCode);
              if (result == 0)  {
                  result = Short.compare(prefix, pn.prefix);
                  if (result == 0)
                      result = Short.compare(lineNum, pn.lineNum);
              }
              return result;
          }
      }
    
    • 중요도에 따라 우선순위로 비교 가능

비교자 생성 메서드(comparator construction method)

  • 자바 8 부터는 Comparator 인터페이스가 비교자 생성 메서드를 이용해 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다.
  • 방식은 간결하지만 성능은 떨어진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class PhoneNumber implements Comparable<PhoneNumber> {
    private short areaCode, prefix, lineNum;

    // 비교자 생성 메서드를 활용한 비교자 (92page)
    private static final Comparator<PhoneNumber> COMPARATOR =
            comparingInt((PhoneNumber pn) -> pn.areaCode)
                    .thenComparingInt(pn -> pn.getPrefix())
                    .thenComparingInt(pn -> pn.lineNum);
//
//    @Override
//    public int compareTo(PhoneNumber pn) {
//        return COMPARATOR.compare(this, pn);
//    }
}
  • 자바에서 타입추론을 제대로 하지 못하기 때문에 명시해 줄 필요가 있다.

정적 메서드 혹은 비교자 생성 메서드를 활용하자.

객체간 순서를 정한다고 해시코드를 기준으로 정렬하기도하는데 단순히 첫 번째 값이 크면 양수, 같으면 0, 첫 번째 값이 작으면 음수를 반환한다는 것만 생각해서 다음과 같이 작성을해선 안된다.

1
2
3
4
5
static Comparator<Object> hashCodeOrder = new Comparator<>() {
	public int compare(Object 01, Object 02) {
		return ol.hashCode() - o2.hashCode();
	}
}

이런 방식은 얼핏보면 문제 없을 것 같지만 정수 오버플로 혹은 IEEE754 부동소수점 계산 방식에 따른 오류를 낼 수 있다. 게다가 속도가 엄청 빠르지도 않다.

대신, 다음처럼 정적 compare메서드 혹은 비교자 생성 메서드를 활용해보자.

1
2
3
4
5
static Comparator<Object hashCodeOrder = new Comparator<>() {
	public int compare(Object o1, Object 02){
		return Integer.compare(01.hashCode(), o2.hashCode());
	}
}
1
2
static Comparator<Object> hashCodeOrder = 
		Comparator.comparingInt(Object::hashCode);

Comparable 구현 클래스를 확장할 때 주의점

기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가했다면 compareTo 규약을 지킬 방법이 없다.

객체 지향적 추상화의 이점을 포기해서 뷰 메서드를 활용해 우회한 코드는 아래와 같다.

Point 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point implements Comparable<Point> {
    private int x;
    private int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public int compareTo(Point point) {
        int result = Integer.compare(x, point.x);
        if (result == 0) {
            return Integer.compare(y, point.y);
        }
        return result;
    }
}

NamedPoint 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class NamedPoint implements Comparable<NamedPoint> {
    // 원래 클래스의 인스턴스를 가르키는 필드
    private Point point;

    private String name;

    public NamedPoint(Point point, String name) {
        this.point = point;
        this.name = name;
    }
    // 내부 인스턴스를 반환하는 '뷰' 메서드
    public Point viewPoint() {
        return point;
    }

    @Override
    public int compareTo(NamedPoint namePoint) {
        int result = point.compareTo(namePoint.point);
        if (result == 0) {
            return name.compareTo(namePoint.name);
        }
        return result;
    }
}

이렇게 하면 NamedPoint 클래스에 원하는 compareTo 메서드를 새롭게 구현할 수 있게 되어 구체 클래스 Point에서 compareTo 일반 규약을 지킬 수 있게 된다.

Test 코드로 검증하면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class PointTest {

    @DisplayName("compareTo의 일반 규약을 지킬 수 있다")
    @Test
    void test() {
        Point point = new Point(1, 3);
        NamedPoint namedPoint = new NamedPoint(new Point(1, 2), "food");

        assertThat(point.compareTo(namedPoint.viewPoint())).isEqualTo(1);
        assertThat(namedPoint.viewPoint().compareTo(point)).isEqualTo(-1);
    }

}

정리

  • 순서를 고려해야하는 값 클래스는 Comparable인터페이스를 꼭 구현하면 좋다.
  • compareTo 메서드에서는 < , >같은 연산자는 쓰지 않아야 한다.
  • 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 활용하자.
This post is written by PRO.