item 14 : Comparable 을 구현할지 고려하라
1. compareTo
ConpateTo란?
3장의 다른 메서드들과 달리 compareTo는 Object의 메서드가 아니다.
성격은 2가지를 제외하면, Object equals와 같음
compareTo 를 구현했다는 것은, 순서가 존재하다는 것이고, Arrays.sort 를 활용한 정렬이 가능하다는 것
보통은 sql에서 order by로 한다는 것
에 자동 정렬되는 TreeSet 자료구조 상 출력하면 알파벳순으로 정렬되어 출력
사실상 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입(아이템 34)이 Comparable을 구현했다. 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.
2. CompareTo 일반규약
한 눈의 정리 🔥
대칭성:
x.compareTo(y)
가 음수면,y.compareTo(x)
는 양수여야 하고,x.compareTo(y)
가 0이면y.compareTo(x)
도 0이어야 한다.추이성:
x.compareTo(y)
가 양수이고,y.compareTo(z)
가 양수라면,x.compareTo(z)
도 양수여야 한다.일관성:
x.compareTo(y)
가 0이면,x.compareTo(z)
와y.compareTo(z)
는 같은 값을 가져야 한다.
부가적인 내용 ⚡
모든 객체에 대해 전역 동치관계를 부여하는 equals 메서드와 달리, compareTo
는 타입이 다른 객체를 신경 쓰지 않아도 된다.
타입이 다른 객체가 주어지면 간단히 ClassCastException
을 던져도 된다. compareTo 규약을 지키지 못하면 비교를 활용하는 클래스와 어울리지 못한다.
compareTo
메서드의 구현과 equals
와의 일관성 유지의 중요성
compareTo
메서드의 구현과 equals
와의 일관성 유지의 중요성그러나 compareTo
의 순서와 equals
의 결과가 일치하지 않는 클래스도 여전히 동작은 한다. 다만, 이러한 클래스를 정렬된 컬렉션(예: TreeSet
, TreeMap
)에 넣으면 해당 컬렉션이 구현한 인터페이스(Collection
, Set
, Map
)의 동작과 엇박자가 발생할 수 있다. 이는 이 인터페이스들이 equals
메서드의 규약을 따르도록 정의되어 있지만, 정렬된 컬렉션은 동치성 비교 시 equals
대신 compareTo
를 사용하기 때문이다.
권장 사항:
가능하다면
x.equals(y)
가true
일 때x.compareTo(y)
는 반드시0
을 반환하도록 구현해야 한다.반대로
x.compareTo(y)
가0
일 때x.equals(y)
가true
가 되도록 구현하면 더욱 좋다.
BigDecimal
클래스의 예시
BigDecimal
클래스의 예시compareTo
와 equals
가 일관되지 않는 클래스로 BigDecimal
을 예로 들 수 있다.
설명:
HashSet
에new BigDecimal("1.0")
과new BigDecimal("1.00")
을 추가하면,equals
메서드로 비교하여 두 객체는 다르다고 판단하므로HashSet
의 원소는 2개가 된다.반면에
TreeSet
은compareTo
메서드로 비교하여 두 객체를 같다고 판단하므로TreeSet
의 원소는 1개가 된다.이로 인해
HashSet
과TreeSet
에서 같은BigDecimal
객체를 사용하더라도 원소의 개수가 달라지는 문제가 발생한다.
🤔 음.. 이거는 HashSet과 TreeSet 의 자료구조의 문제 아냐?
답변 : 이러한 현상은 BigDecimal
클래스의 설계와 컬렉션 프레임워크의 동작 방식의 차이로 인해 발생하는 것입니다. 일반적으로 equals()
와 compareTo()
메서드는 일관되게 동작하도록 구현하는 것이 권장됩니다. 즉, equals()
로 같다고 판단되는 객체는 compareTo()
로 비교했을 때 0
을 반환해야 합니다.
하지만 BigDecimal
클래스는 정밀도(precision)를 중요시하기 때문에 equals()
메서드에서 스케일까지 고려하도록 설계되었습니다. 반면에 수치적인 비교를 위한 compareTo()
는 스케일을 무시하고 값만 비교하도록 되어 있습니다.
따라서 이는 클래스의 설계 의도에 따른 결과이며, 컬렉션 프레임워크의 동작과 충돌할 수 있는 부분입니다. 이러한 차이로 인해 발생하는 문제는 개발자가 인지하고 조심해야 할 부분이지, Java 언어나 컬렉션 프레임워크의 구조적인 결함이라고 보기는 어렵습니다.
따라서, 구조적인 문제라기보다는 해당 클래스와 컬렉션 사용 시 주의해야 할 사항으로 보는 것이 맞습니다
🤔 하지만 위의 코드는 compareTo메서드가 아니라 size라는 메서드를 쓴거 아닌가?
예제 코드에서
size()
메서드를 사용한 것은 집합에 실제로 몇 개의 원소가 저장되었는지 확인하기 위한 것입니다.원소의 개수가 달라지는 이유는
HashSet
과TreeSet
이 원소의 동일성을 판단하는 기준이 다르기 때문입니다.HashSet
은equals()
와hashCode()
를 사용하고,TreeSet
은compareTo()
를 사용합니다.BigDecimal
클래스의equals()
와compareTo()
메서드가 일관되지 않게 동작하기 때문에 이런 차이가 발생합니다.
HashSet
의 동작 원리
HashSet
에 원소를 추가할 때, 원소의hashCode()
값을 사용하여 버킷을 결정합니다.이미 같은
hashCode()
를 가진 원소가 있는 경우,equals()
메서드를 사용하여 두 원소가 동일한지 비교합니다.equals()
메서드가false
를 반환하면, 해당 원소는 집합에 새로운 원소로 추가됩니다.
2. TreeSet
의 동작 원리
TreeSet
은 이진 탐색 트리를 기반으로 구현되어 있으며, 원소를 정렬된 순서로 유지합니다.원소를 추가할 때,
compareTo()
메서드나 제공된Comparator
를 사용하여 원소의 순서와 동일성을 판단합니다.compareTo()
메서드가0
을 반환하면, 두 원소는 동일한 것으로 간주되어 새로운 원소로 추가되지 않는다.
해결 방법 🍁
compareTo()
와equals()
의 구현을 일관되게 수정한다.하지만
BigDecimal
클래스는 Java 표준 라이브러리의 클래스이므로 우리가 수정할 수 없다.
커스텀 Comparator를 사용하여
TreeSet
의 동작을 수정한다.스케일까지 고려하도록 Comparator를 정의하면
TreeSet
에서도 원소의 개수가 2개가 됨
compareTo
메서드를 올바르게 구현하고,equals
와의 일관성을 유지하는 방법을 이해할 수 있다. 이는 코드의 신뢰성과 유지보수성을 높이고, 컬렉션 프레임워크를 사용할 때 발생할 수 있는 오류를 예방하는 데 중요
🔥 컬렉션 사용 시 주의사항
컬렉션에 원소를 추가할 때, 해당 컬렉션이 원소의 동일성을 어떤 기준으로 판단하는지 알아야 한다.
equals()와 compareTo() 메서드의 구현이 일관되지 않으면 컬렉션에서 예기치 않은 동작이 발생할 수 있다.
3. compareTo
메서드 작성 요령
compareTo
메서드 작성 요령compareTo
메서드를 작성할 때는 equals
메서드와 비슷한 요령을 따르지만, 몇 가지 차이점을 주의해야 한다.
타입 검사와 형변환 불필요:
Comparable
은 제네릭 인터페이스이므로,compareTo
메서드의 인수 타입은 컴파일 타임에 정해진다.인수의 타입이 잘못되면 컴파일 자체가 되지 않으므로, 런타임 타입 검사나 형변환이 필요 없다.
null
처리:compareTo
메서드에null
을 인수로 넣으면NullPointerException
을 던지는 것이 일반적이다.실제로도 인수(
null
)의 멤버에 접근하려는 순간 이 예외가 발생한다.
필드 비교 방법:
compareTo
메서드는 각 필드가 동치인지를 비교하는 것이 아니라, 순서를 비교한다.기본 타입 필드는
<
,>
연산자를 사용하여 비교한다.객체 참조 필드는 해당 클래스의
compareTo
메서드를 재귀적으로 호출하여 비교한다.Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 하는 경우에는
Comparator
를 사용한다.
4. compareTo
메서드 구현과 Comparator
활용
compareTo
메서드 구현과 Comparator
활용1) 여러 필드를 비교하는 compareTo
메서드 구현
compareTo
메서드 구현클래스에 핵심 필드가 여러 개라면 어느 것을 먼저 비교하느냐가 중요해진다. 가장 중요한 필드부터 비교해나가는 것이 좋다.
비교 결과가 0이 아니라면, 즉 순서가 결정되면 그 결과를 곧장 반환한다.
가장 중요한 필드가 같다면, 그다음으로 중요한 필드를 비교해나간다.
예시: PhoneNumber
클래스의 compareTo
메서드
short.compare
를 사용하여short
타입 필드를 비교한다.이렇게 하면 코드가 간결해지고, 오버플로우 등의 문제를 방지할 수 있다.
2) Comparator
생성 메서드를 활용한 compareTo
구현
Comparator
생성 메서드를 활용한 compareTo
구현자바 8부터는 Comparator
인터페이스에 비교자 생성 메서드들이 추가되어, 메서드 연쇄 방식으로 비교자를 생성할 수 있다. 이를 활용하면 compareTo
메서드를 더 간결하게 구현할 수 있다.
예시: PhoneNumber
클래스의 compareTo
메서드
comparingInt
메서드는 키 추출 함수를 받아 그 키를 기준으로 비교하는Comparator
를 생성thenComparingInt
메서드를 사용하여 추가적인 필드를 순차적으로 비교한다.
주의사항:
이 방식은 코드의 간결함을 제공하지만, 약간의 성능 저하가 있을 수 있다. 테스트 결과 약 10% 정도 느려질 수 있다.
따라서 성능이 중요한 상황에서는 전통적인 방식으로 구현하는 것이 좋을 수 있습니다.
전통방식
3) Comparator
의 다양한 메서드 활용
Comparator
의 다양한 메서드 활용Comparator
는 다양한 보조 생성 메서드들을 제공한다.
숫자 타입 필드를 비교하기 위한 메서드:
comparingInt
,comparingLong
,comparingDouble
thenComparingInt
,thenComparingLong
,thenComparingDouble
객체 참조 타입 필드를 비교하기 위한 메서드:
comparing
메서드: 키 추출자를 받아 키의 자연 순서나 지정한Comparator
로 비교thenComparing
메서드: 추가적인 비교 기준을 지정
예시: 객체 참조 필드 비교
Person
클래스의lastName
,firstName
,age
필드를 순차적으로 비교
4) 잘못된 compareTo
구현 방식 피하기
compareTo
구현 방식 피하기가끔 값의 차이를 반환하여 비교하는 compareTo
메서드를 볼 수 있다.
이 방식은 오버플로우나 언더플로우가 발생할 수 있어 신뢰할 수 없다.
또한, 부동소수점 타입에서는 정밀도 손실이 발생할 수 있다.
올바른 구현 방식:
정적
compare
메서드를 사용또는
Comparator
생성 메서드를 활용한다.
5) 올바른 비교자
정적 compare
메서드 활용:
Integer.compare
를 사용하여 오버플로우 없이 안전하게 비교한다.
2. 비교자 생성 메서드 활용:
Comparator.comparingInt
를 사용하여 더욱 간결하게 구현한다.
유의 사항
compareTo
메서드에서 필드를 비교할 때는:기본 타입 필드는 해당 박싱 클래스의 정적
compare
메서드를 사용객체 참조 필드는 해당 필드의
compareTo
메서드를 재귀적으로 호출한다.Comparable
을 구현하지 않은 필드는 적절한Comparator
를 사용한다.
필드의 비교 순서는 중요도에 따라 정하기
가장 중요한 필드부터 비교하여 순서가 결정되면 즉시 반환
Comparator
생성 메서드를 사용하면 코드가 간결해짐그러나 성능 저하가 있을 수 있으므로 상황에 맞게 선택
값의 차이를 반환하는 방식은 피해야 한다.
오버플로우, 언더플로우 등의 문제를 일으킬 수 있다.
compareTo
메서드 구현 시:정적 비교자 생성 메서드를 활용하여 간결하고 명확한 코드를 작성한다.
필드의 중요도 순서에 따라 비교를 진행한다.
타입 추론의 한계로 인해 람다 표현식에서 타입을 명시해야 할 때가 있으니 주의해야 한다.
✨ 결론
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러 지도록 해야 한다.
compareTo 메서드에서 필드의 값을 비교할 때 <와 > 연산자는 쓰지 말아야 한다.
그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
Last updated