Item14 - Comparable을 구현할지 고민하라

2023. 11. 25. 18:11Book/이펙티브 자바

 

 

Comparable을 구현하면 이 인터페이스를 활용하는 수많은 제네릭 알고리즘과 컬렉션의 힘을 누릴 수 있다. 순서가 중요한 값 클래스를 작성한다면 Comparable도 구현하자.

 

규약

 - Object.equals에 더해서 순서까지 비교할 수 있으며 Generic 지원

 - 자기 자신이 compareTo에 전달된 객체보다 작으면 음수, 같으면 0, 크다면 양수를 리턴한다.

 - 반사성, 대칭성, 추이성을 만족해야 한다.

 - 반드시 따라야 하는 것은 아니지만 x.compareTo(y) == 0 이라면 x.equals(y) 가 true여야 한다.

   => 동작은 하지만 이 클래스의 객체를 정렬된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스에 정의된 동작과 엇박          자를 낼 수 있음

 

 

public class CompareToConvention {

    public static void main(String[] args) {
        BigDecimal n1 = BigDecimal.valueOf(2313);
        BigDecimal n2 = BigDecimal.valueOf(1123);
        BigDecimal n3 = BigDecimal.valueOf(5354);
        BigDecimal n4 = BigDecimal.valueOf(1123);

        //반사성
        System.out.println(n1.compareTo(n1));

        //대칭성
        System.out.println(n1.compareTo(n2));
        System.out.println(n2.compareTo(n1));

        //추이성
        System.out.println(n3.compareTo(n1) > 0);
        System.out.println(n1.compareTo(n2) > 0);
        System.out.println(n3.compareTo(n2) > 0);

        //일관성
        System.out.println(n4.compareTo(n2));
        System.out.println(n2.compareTo(n1));
        System.out.println(n4.compareTo(n1));

        //compareTo가 0이라면 eqauls는 true여야 한다. BigDecimal은 그렇진 않다
        BigDecimal oneZero = new BigDecimal("1.0");
        BigDecimal oneZeroZero = new BigDecimal("1.00");

        System.out.println(oneZero.compareTo(oneZeroZero));
        System.out.println(oneZero.equals(oneZeroZero));
    }
}

BigDecimal eqauls 문서

Compares this BigDecimal with the specified Object for equality.
Unlike compareTo, this method considers
two BigDecimal objects equal only if they are equal in value and
scale (thus 2.0 is not equal to 2.00 when compared by this method).

 

 

 

구현 방법

 - 자연적인 순서를 제공할 클래스에 implements Comparatable<T> 선언

 - compareTo 메서드 재정의 (자기 자신과 매개변수 비교)

 - compareTo 메서드 안에서 기본 타입은 박싱된 기본 타입의 compare을 사용해 비교

 - 핵심 필드가 여러 개라면 비교 순서가 중요하다. 순서를 결정하는데 있어서 가장 중

   요한 필드를 비교하고 그 값이 같다면(0) 다음 필드 비교

public class Point implements Comparable<Point>{

    final int x, y;

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

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

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

 

 - 기존 클래스를 확장하고 필드를 추가하는 경우 규약을 지킬 수 없음 => 컴포지션 사용 추천

public class NamedPoint implements Comparable<NamedPoint> {

    private final Point point;
    private final String name;

    public NamedPoint(Point point, String name) {
        this.point = point;
        this.name = name;
    }

	/view
    public Point getPoint() {
        return this.point;
    }

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

 

간단하게 비교자 생성 메서드를 활용할 수도 있다.

    private static final Comparator<PhoneNumber> COMPARATOR =
            comparingInt((PhoneNumber pn) -> pn.areaCode)
                    .thenComparingInt(pn -> pn.prefix)
                    .thenComparingInt(pn -> pn.lineNum);
    @Override
    public int compareTo(PhoneNumber pn) {
        return COMPARATOR.compare(this, pn);
    }

 

 

비교 연산자를 사용할 때 주의할 점이 한가지 있다. 두개의 int 값을 받아 '-' operator를 통해 값을 비교할 때 자료형의 범위를 벗어나면 언더플로우가 발생하거나, '+' operator를 통해 비교할 때 오버플로우가 일어나 양수 음수가 바껴 기대값이 반대로 나올 수도 있다 그러니 정적 compare를 활용해 비교하도록 하자(Integer.compare(a, b))