public interface Comparable<T> {
int compareTo(T t);
}
Comparable을 구현했다는 것은 그 클래스의 인스턴스에는 자연적인 순서가 있음을 뜻함
검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게 할 수 있음, 알파벳순 출력도 가능함
알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현해야함
compareTo 메서드의 일반 규약은 equals의 규약과 비슷함
ClassCastException
을 던짐sgn
은 수학에서 말하는 부호 함수를 뜻하며, 표현식의 값이 음수, 0, 양수일 때 -1,0,1을 반환하도록 정의함compareTo의 경우 타입이 다른 객체를 신경 쓰지 않아도 됨, 간단히 예외를 던지기 때문에
다른 타입 사이의 비교도 혀용하는데, 보통은 비교할 객체들이 구현한 공통 인터페이스를 매개로 이뤄짐
여기서 compareTo 규약을 지키지 못하면 비교를 활용하는 클래스와 어울리지 못함(TreeSet, TreeMap, Collections, Arrays 등)
위의 규약 중 첫 번째 규약은 순서를 바꿔 비교해도 예상한 결과가 나와야함, 두 번째 규약은 말 그대로 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다는 것이고 마지막 규약은 크기가 같은 객체들끼리는 어떤 객체와 비교해도 항상 같아야 한다는 것임
이는 동치성 검사도 equals 규약과 똑같이 반사성, 대칭성, 추이성을 충족해야 함을 뜻함, 그래서 주의사항도 똑같음
새로운 값 컴포넌트를 추가했다면 compareTo 규약을 지킬 방법이 없음
우회법도 같음, Comparable을 구현한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면 독립된 클래스를 만들고 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두면 됨, 그런 다음 내부 인스턴스를 반환하는 뷰 메서드를 제공하면 됨
이렇게 해서 바깥 클래스에 우리가 원하는 compareTo 메서드를 구현해 넣을 수 있음, 그리고 필요에 따라 클라이언트가 인스턴스를 다루기 용이해짐
마지막 규약인 compareTo 메서드로 수행한 동치성 결과가 equals와 같아야 하는 것임, 이를 지키지 않으면 정의된 동작과 엇박자가 남, 컬렉션의 경우 equals의 규약도 따르지만 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());