본문 바로가기
JAVA/Effective Java

item 14. Comparable을 구현할지 고려하라

by Garonguri 2022. 4. 23.
728x90

[ compare to ]

- Comparable 인터페이스의 유일무이한 메서드

- Object 메서드가 아니다!

- 단순 동치성 뿐만 아니라, 순서까지 비교할 수 있다. (즉, Comparable을 구현한 클래스의 인스턴스는 자연적 순서가 생긴다.)

 

Comparable을 구현한 객체의 정렬 방법

 : Arrays.sort(객)

 

- 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이 Comparable을 구현했다.

- 순서가 명확한 값 클래스를 작성한다면 Comparable interface를 구현해야 한다.

 

[ 일반 규약 ]

해당 객체와 주어진 객체의 순서를 비교한다.

객체가 매개변수로 들어온 객체보다 작으면 음의 정수(-1), 같으면(0), 크다면 양의 정수(+1)을 반환
객체와 매개변수로 들어온 객체의 타입이 다르다면 ClassCastException

1. x.compareTo(y) == -y.compareTo(x)
: 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다
즉, x.compareTo(y)가 1이라면 y.compareTo(x)는 -1이여야 하는 것.

2.(x.compareTo(y) > 0 && y.compareTo(z) > 0) == x.compareTo(z) > 0
: 추이성을 보장해야 한다.

3. x.compareTo(y) == 0 == (x.compareTo(z) == y.compareTo(z))

4. (x.compareTo(y) == 0) == x.eqauls(y)
: 동치성을 보장해야 한다.
즉, 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다. 필수가 아니지만, 꼭 지키는것이 좋다.

- equals 규약과 같이 반사성, 대칭성, 추이성을 만족해야 한다.

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

  • 해결 방법 : 클래스를 확장하는 대신 독립된 클래스를 만들고, 원래 클래스의 인스턴스를 가리키는 필드를 만든다. 그 후 내부 인스턴스를 반환하는 뷰 메서드를 제공하는 방식으로 compareTo를 구현하면 된다.

 

- compareTo 규약을 지키지 못한다면, 비교를 활용하는 클래스와 호환되지 않는다.

(TreeSet, TreeMap, Collections, Arrays 등의 정렬,검색 등을 사용할 수 없음)

HashSet : 데이터를 중복 저장할 수 없음.입력 순서를 보장하지 않음
TreeSet : 중복된 데이터를 저장할 수 없음. 입력 순서대로 데이터를 저장하지 않음. 기본적으로. 오름차순으로 데이터 정렬
LinkedHashSet : 중복된 데이터를 저장할 수 없음. 입력된 순서대로 데이터 저장.

[ compareTo method 작성 요령 ]

 

- equals와 비슷하지만 차이점 존재.

- Comparable은 타입을 인수로 받는 제네릭 인터페이스이다. compareTo 메서드의 인수 타입은 컴파일 타임에 정해진다.

- 즉, 입력 인수의 타입을 확인하거나 형변환할 필요가 없다. (타입이 잘못되면 어짜피 컴파일 안됨)

- compareTo 메서드는 각 필드가 동치인지를 비교하는게 아니라, 순서를 비교한다.

- 객체 참조 필드를 비교하려면 compareTo 메소드를 재귀적으로 호출한다.

- Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교하고 싶다면 비교자 Comparator 사용.

 

public class item14 {

    public static void main(String[] args) {

        Students[] student = new Students[4];

        student[0] = new Students("Ga", 20221234, 99.5);
        student[1] = new Students("Jong", 20221212, 99.6);
        student[2] = new Students("Dae", 20221357, 99.8);
        student[3] = new Students("Sung", 20224321, 99.7);

        Arrays.sort(student);
        System.out.println(Arrays.toString(student));

        System.out.println("---------------------------");
        Comparator<Students> comparator = new Comparator<Students>() {
            @Override
            public int compare(Students s1, Students s2) {
                return Double.compare(s1.Score, s2.Score);
            }
        };

        Arrays.sort(student, comparator);
        System.out.println(Arrays.toString(student));

    }

    public static class Students implements Comparable<Students>{
        String Name;
        int Id;
        double Score;

        public Students(String name, int id, double score) {
            Name = name;
            Id = id;
            Score = score;
        }

        @Override
        public int compareTo(Students o) {
            return this.Id-o.Id;
        }

        @Override
        public String toString() {
            return "Students{" +
                    "Name='" + Name + '\'' +
                    ", Id=" + Id +
                    ", Score=" + Score +
                    '}';
        }
    }
}

주의 ! compareTo 메서드에서 <, >를 사용하는 방식은 거추장스럽고 오류를 유발한다.


[ Comparator ]

비교자를 직접 만들거나 자바가 제공하는 것을 쓰자.

- 객체 참조용 비교자를 활용하는 경우

: 단순히 필드 하나에 대해 compare로 비교하기 어려운 경우.

private static final Comparator<Students> COMPARATOR
        = Comparator.comparingInt((Students student) -> student.Id).
        thenComparingDouble((Students student) -> student.Score).
        thenComparing((Students student) -> student.Name);

@Override
public int compareTo(Students o) {
    return COMPARATOR.compare(this, o);
}

위와 같이 비교자 생성 메소드를 통해 비교연산을 다시 할 수 있다.


해시 코드를 통해 해시 코드 값의 차를 기준으로 비교할 수도 있음.

[잘못된 코드]

- 오버플로우를 일으킬 수 있다. 부동소수점 계산 방식에 따른 오류도 일어날 수 있음.

//추이성을 위배하는 코드. 쓰지 마라.
private static final Comparator<Students> HASHCODE_COMPARATOR = new Comparator<Students>() {
    @Override
    public int compare(Students o1, Students o2) {
        return o1.hashCode() - o2.hashCode();
    }
};

[올바른 코드]

//1.
private static final Comparator<Students> HASHCODE_COMPARATOR = new Comparator<Students>() {
    @Override
    public int compare(Students o1, Students o2) {
        return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};
//2.
private static final Comparator<Students> HASHCODE_COMPARATOR = Comparator.comparingInt(
        Object::hashCode);

 

핵심 정리

순서를 고려해야 하는 값 클래스를 작성하려면 Comparable interface를 구현하자.
<, >연산자는 사용하지 말자.
박싱된 기본 타입 클래스가 제공하는 정적 compare 메소드나 Comparator 인터페이스가 제공하는 비교자 생성 메소드를 이용해보자.
728x90

댓글