Language/Java

[effective java] 아이템 14. Comparable을 구현할지 고려하라.

JOYERIM 2023. 3. 25. 02:21

 

 

 

 

 

 

핵심 요약

 

 

 

 

 

순서를 고려해야 하는 클래스를 작성할 때, Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬, 검색, 비교 기능을 제공하는 컬렉션과 어우러지도록 하자.

 

비교를 활용하는 클래스의 예는 정렬된 컬렉션인 TreeSetTreeMap, 검색과 정렬 알고리즘을 활용하는 유틸리티 클래스인 CollectionsArrays가 있다.

 

compareTo 메소드에서 필드의 값을 비교할 때, <> 연산자를 쓰면 안된다.

 

그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메소드Comparator 인터페이스가 제공하는 비교자 생성 메소드를 사용하자.

 

 

 

 

 

 

 

 

 


 

 

 

 

Comparable 인터페이스

 

 

 

 

Comparable 인터페이스의 형태는 아래와 같다.

 

 

 

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

 

 

 

 

 

 


 

 

 

 

 

 

 

compareTo 메소드의 일반 규약
: equals와 동일하게 반사성, 대칭성, 추이성을 만족해야 함

 

 

 

 

 

이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을,크면 양의 정수를 반환한다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다. 

 

(이해의 편의를 위해 음수, 0, 양수일 때 -1, 0, 1을 반환한다고 가정하였다.)

 

 

 

대칭성

Comparable을 구현한 클래스는 모든 x, y에 대해

x.compareTo(y) == -y.compareTo(x)

 

 

 

추이성

Comparable을 구현한 클래스는 모든 x, y에 대해

(x.compareTo(y) > 0 && y.compareTo(z) > 0) 👉 x.compareTo(z) > 0

 

 

 

Comparable을 구현한 클래스는 모든 z에 대해

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

 

 

 

크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다.

 

 

 

필수는 아니지만 꼭 지키는 게 좋다. 지키지 않는 클래스는 "주의: 이 클래스의 순서는 equals 메소드와 일관되지 않다"처럼 명시하자.

(x.compareTo(y) == 0) == (x.equals(y))

 

 

 

compareTo 메소드에서 객체의 비교 순서와 equals 메소드에서 객체의 동치성 비교 결과가 일관되지 않으면, 정렬된 컬렉션에 객체를 추가할 때 문제가 발생할 수 있다.

 

Java에서 Collection, Set, Map 인터페이스들은 equals 메소드의 규약을 따르지 않는다. 컬렉션에 객체를 추가하거나 삭제할 때, equals 메소드를 사용하지 않고 compareTo 메소드를 사용한다.

 

 

 

 

주의사항
: equals와 유사

 

 

 

 

기존 클래스를 확장하면서 새로운 값 컴포넌트를 추가할 때, Comparable 규약을 지키기 어렵다.

 

 

기존 클래스를 확장하면서 새로운 값 컴포넌트를 추가하게 되면, compareTo 메소드가 새로운 값 컴포넌트를 비교하지 못해, Comparable 규약을 위반하게 된다.

 

해결 방법은 컴포지션을 이용하는 것이다. 새로운 클래스는 기존 클래스의 인스턴스를 필드에 저장하고, 새로 추가된 값 컴포넌트를 비교하는 compareTo 메소드를 제공한다.

 

또한, 새로운 클래스에서 원래 클래스의 인스턴스를 반환하는 뷰 메소드를 제공하여, 클라이언트가 필요에 따라 원래 클래스의 인스턴스를 다룰 수 있도록 한다.

 

 

 

아래 클래스는 이름, 나이를 우선순위로 정렬하는 클래스이다. 이때 email 필드를 추가하려고 한다.

 

public class Person implements Comparable<Person> {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    @Override
    public int compareTo(Person other) {
        int nameCompare = name.compareTo(other.name);
        if (nameCompare != 0) {
            return nameCompare;
        }
        return Integer.compare(age, other.age);
    }
}

 

 

 

아래 클래스는 Person 객체를 비교한 후, email을 비교한다. 또한, Person 객체를 반환하는 뷰 메소드도 제공한다.

 

 

public class PersonWithEmail implements Comparable<PersonWithEmail> {
    private Person person;
    private String email;
    
    public PersonWithEmail(Person person, String email) {
        this.person = person;
        this.email = email;
    }
    
    public Person getPerson() {
        return person;
    }
    
    public String getEmail() {
        return email;
    }
    
    @Override
    public int compareTo(PersonWithEmail other) {
        int personCompare = person.compareTo(other.person);
        if (personCompare != 0) {
            return personCompare;
        }
        return email.compareTo(other.email);
    }
    
    public static PersonWithEmail fromPerson(Person person, String email) {
        return new PersonWithEmail(person, email);
    }
}

 

 

 

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

 

 

입력 인수의 타입이 잘못되면 컴파일 자체가 되지 않기 때문에, 타입을 확인하거나 형변환하지 않아도 괜찮다.

 

만약 null을 넣을 경우 NullPointerException을 던져야 한다.

 

 

 

 

 

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

직접 구현하는 방법과 자바가 제공하는 것을 사용하는 방법이 있다.

 

 

 

 

관계 연산자 <와 >를 사용하지 말고 정적 메소드인 compare을 이용하자.

 

 

앞서 다룬 코드의 일부이다.

 

@Override
    public int compareTo(Person other) {
        int nameCompare = name.compareTo(other.name);
        if (nameCompare != 0) {
            return nameCompare;
        }
        return Integer.compare(age, other.age);
    }

 

 

 

책에서 권장하는 방식은 아래 2가지 방법이다.

 

 

 

 

✍ 해결 방법 1 : 정적 compare 메소드를 활용한 Comparator

 

 

 

Comparator<Person> ageComparator = new Comparator<Person>() {
    public int compare(Person p1, Person p2) {
        return Integer.compare(p1.getAge(), p2.getAge());
    }
};

 

 

 

 

✍ 해결 방법 2 : Comparator 생성 메소드를 활용한 비교자

 

 

public static Comparator<Person> compareByAge() {
        return Comparator.comparing(Person::getAge);
    }

 

 

 

 

 


 

 

 

 

 

 

 

자바 [JAVA] - Comparable 과 Comparator의 이해

아마 이 글을 찾아 오신 분들 대개는 Comparable과 Comparator의 차이가 무엇인지 모르거나 궁금해서 찾아오셨을 것이다. 사실 알고보면 두 개는 그렇게 어렵지 않으나 아무래도 자바를 학습하면서 객

st-lab.tistory.com