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

gang_shik·2022년 3월 3일
0

Effective Java 3장

목록 보기
5/5
  • Comparable 인터페이스의 유일한 메서드 compareTo는 Object 메서드가 아니며 단순 동치성 비교에 더해 순서까지 비교할 수 있으며 제네릭함
public interface Comparable<T> {
	int compareTo(T t);
}
  • Comparable을 구현했다는 것은 그 클래스의 인스턴스에는 자연적인 순서가 있음을 뜻함

  • 검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게 할 수 있음, 알파벳순 출력도 가능함

  • 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현해야함

  • compareTo 메서드의 일반 규약은 equals의 규약과 비슷함

    • 이 객체와 주어진 객체의 순서를 비교함, 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환함
    • 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던짐
    • 아래에서 나오는 sgn은 수학에서 말하는 부호 함수를 뜻하며, 표현식의 값이 음수, 0, 양수일 때 -1,0,1을 반환하도록 정의함
    • Comparable을 구현한 클래스는 모든 x,y에 대해 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))여야함(따라서 x.compareTo(y)는 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))여야 함, Comparable을 구현하고 이 권고를 지키지 않는 모든 클래스는 그 사실을 명시해야함(이 클래스의 순서는 equals메서드와 일관되지 않음)
  • compareTo의 경우 타입이 다른 객체를 신경 쓰지 않아도 됨, 간단히 예외를 던지기 때문에

  • 다른 타입 사이의 비교도 혀용하는데, 보통은 비교할 객체들이 구현한 공통 인터페이스를 매개로 이뤄짐

  • 여기서 compareTo 규약을 지키지 못하면 비교를 활용하는 클래스와 어울리지 못함(TreeSet, TreeMap, Collections, Arrays 등)

  • 위의 규약 중 첫 번째 규약은 순서를 바꿔 비교해도 예상한 결과가 나와야함, 두 번째 규약은 말 그대로 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다는 것이고 마지막 규약은 크기가 같은 객체들끼리는 어떤 객체와 비교해도 항상 같아야 한다는 것임

  • 이는 동치성 검사도 equals 규약과 똑같이 반사성, 대칭성, 추이성을 충족해야 함을 뜻함, 그래서 주의사항도 똑같음

  • 새로운 값 컴포넌트를 추가했다면 compareTo 규약을 지킬 방법이 없음

  • 우회법도 같음, Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면 독립된 클래스를 만들고 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두면 됨, 그런 다음 내부 인스턴스를 반환하는 뷰 메서드를 제공하면 됨

  • 이렇게 해서 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현해 넣을 수 있음, 그리고 필요에 따라 클라이언트가 인스턴스를 다루기 용이해짐

  • 마지막 규약인 compareTo 메서드로 수행한 동치성 결과가 equals와 같아야 하는 것임, 이를 지키지 않으면 정의된 동작과 엇박자가 남, 컬렉션의 경우 equals의 규약도 따르지만 compareTo로 동치성을 비교하기 때문에


compareTo 메서드 작성 요령

  • equals와 비슷함, 몇 가지 차이점만 주의하면 됨

  • Comparable이 제네릭 인터페이스여서 컴파일 타임인수 타입이 정해져 타입 확인과 형변환 필요가 없다는 것과 null을 인수로 넣어 호출하면 예외를 던져야함

  • compareTo는 각 필드가 동치인지 비교하는게 아니라 그 순서를 비교함, 객체 참조 필드를 비교하려면 compareTo를 재귀적으로 호출해야함

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

  • compareTo 메서드에서 관계연산자 <>를 사용하는 방식은 오류를 유발하여 이제 추천하지 않음, 이젠 박싱된 기본 타입 클래스들에 compare메서드를 이용하는 것을 권장함

  • 클래스에 핵심 필드가 여러개라면 어느 것을 먼저 비교하느냐가 중요해짐, 가장 핵심적인 필드부터 비교하고 비교 결과가 0이 아니라면 순서가 결정되면 끝이고 그 결과를 곧장 반환하면 됨

  • 가장 핵심이 되는 필드가 똑같다면, 똑같지 않은 필드를 찾을 때까지 그 다음으로 중요한 필드를 비교해나감, 아래와 같이 비교 가능

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 인터페이스가 일련의 비교자 생성 메서드와 팀을 꾸려 메서드 연쇄 방식으로 비교자를 생성할 수 있게됨, 그리고 이 비교자들을 Comparable 인터페이스가 원하는 compareTo 메서드를 구현하는데 사용할 수 있음

  • 이때 정적 임포트 기능을 사용하면 깔끔하게 쓸 수 있음, 아래와 같이 쓸 수 있음

private static final Comparator<PhoneNumber> COMPARATOR = 
		comparingInt((PhoneNumber pn) -> pn.areaCode)
        	.thenComparingInt(pn -> pn.prefix)
            .thenComparingInt(pn -> pn.lineNum);
            
public int compareTo(PhoneNumber pn) {
	return COMPARATOR.compare(this, pn);
}
  • 위처럼 비교자 생성 메서드 2개를 이용해 비교자를 생성함, int 타입 키에 매핑해 키 추출 함수를 인수로 받아, 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드임, 이를 람다를 통해서 인수로 받고 반환하여서 처리함, 이때 입력 인수의 타입을 명시함

  • 두 전화번호의 지역 코드가 같을 수 있으므로 비교 방식을 더 다듬어야함, 이 일은 두 번째 생성 메서드인 thenComparingInt가 수행함, 다시 비교자를 반환함, 그래서 위에서 연달아 호출해서 비교를 함, 여기선 타입을 명시하지 않아도 타입 추론 능력이 가능함

비교자 생성 메서드? 메서드 연쇄 방식?

비교자 생성 메서드 & 메서드 연쇄 방식

위에서 COMPARATOR 방식이 비교자 생성 메서드를 통해서 람다로 이어서 그 이름을 통해 메서드를 연속으로 써서 쓰는 메서드 연쇄 방식

타입 추론 능력?

타입 추론 능력

타입 추론 능력은 코드 작성 당시 타입이 정해지지 않았어도 컴파일러가 그 타입을 유추하는 것을 의미함

여기서 타입 추론은 일반 변수에 대해서 지원하지 않고 제네릭람다의 상황에서 타입을 추론함

그래서 이는 컴파일 상황에서 자바가 타입에 대해서 추론하는 내부적인 기능이 있고 이것이 제네릭람다의 상황에서 추론을 하는 것임

그래서 만약 타입 추론이 애매한 상황일 경우 컴파일이 되지 않고 에러가 나기도함

위의 예시 같은 경우 람다에서의 타입 추론이 가능하지만 메서드 호출시 타입이 제거되고 호출이 되는데 위철머 메서드 연쇄 방식을 써버리면 타입 추론이 어려워짐, 그 정도로 타입 추론 능력이 높은 건 아님, 그래서 타입을 명시해준 것

이런 상황은 메서드 호출시 종종 일어난다고 함


보조 생성 메서드

  • Comparator는 수많은 보조 생성 메서드가 존재함, 자바의 숫자용 기본 타입을 모두 커버함

  • 객체 참조용 비교자 생성 메서드도 있음, comparing이라는 정적 메서드 2개가 다중 정의되어 있는데, thenComparing의 경우는 3개 다중 정의가 되어 있음, 이는 키 추출자를 받아서 처리하고 키 추출자 하나와 추출된 키를 비교하는 방식으로 이어짐

  • 여기서 값의 차를 이용하는 compareTo와 compare 메서드가 있는데 이 방식은 사용하면 안됨, 정수 오버플로우나 부동소수점 계산 방식에 따른 오류가 날 수 있어서 아래와 같이 처리하는게 나음

  • 정적 compare 메서드를 활용한 비교자

static Comparator<Object> hashCodeOrder = new Comparator<>() {
	public int compare(Object o1, Object o2) {
    	return Integer.compare(o1.hashCode(), o2.hashCode());
    }
};
  • 비교자 생성 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = 
	Comparator.comparingInt(o -> o.hashCode());
profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글