- 이 글은 Effective Java 를 기반으로 작성되었습니다.
- Comparable 개요
compareTo 메소드는 Comparable 인터페이스에서 유일한 메소드이며, 여태까지 다루었던 메소드들과는 달리 Object 에 선언되어 있지 않다. compareTo 는 equals 와 특성이 비슷하지만 순서 비교가 가능하며 좀 더 일반적이다. 순서 비교가 가능하므로 검색하거나 최대/최소를 계산하고 정렬 상태를 유지할 수 있는 특징이 있다.
Comparable 은 compareTo 메소드 하나만 갖는 간단한 인터페이스이지만 이를 구현하면 다양한 제네릭 알고리즘 및 해당 인터페이스를 사용하도록 설계된 컬렉션 구현체들과도 전부 연동할 수 있어서 매우 강력하다. 따라서 알파벳 순서, 값 크기, 시간 선후관계처럼 명확한 순서를 따르는 값 클래스라면 구현을 고려하는것이 좋다.
public int compareTo(T o);
- compareTo 메소드의 일반 규약
compareTo 메소드의 일반 규약은 특별한 자료를 참조해야하는것이 아니라 Comparable 인터페이스 주석에 모두 기술되어 있다. 아래는 이 내용을 기술한것이다.
compareTo 는 해당 객체와 다른 특정 객체를 비교하며 해당 객체가 다른 객체보다 작거나 같거나 큰 경우 각각 음수, 0, 양수를 반환한다. 아래 규약에서 sgn(signnum) 은 1. 0, -1 중 1개의 값을 반환한다.
- 모든 x와 y에 대해서 sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) 를 만족해야 한다. x.compareTo(y)가 Exception 을 던진다면 y.compareTo(x) 도 Exception 을 발생시켜야 한다.
- 추이성을 만족해야 한다. x.compareTo(y) > 0 && y.compareTo(z) 를 만족하면 x.compareTo(z) 를 만족해야 한다.
- x.compareTo(y) == 0 을 만족하면 모든 z에 대해 sgn(x.compareTo(z)) == sgn(y.compareTo(z)) 을 만족해야 한다.
- 반드시 성립해야 하는 것은 아니지만 x.compareTo(y) == 0 일 때 x.equals(y) 를 만족해야하며, 해당 조건을 만족하지 않을 시 이를 명시해주는것이 좋다.
- compareTo 메소드 구현시 고려사항
Comparable 개요에서 equals 와 특성이 비슷하다고 언급하였는데 일반 규약을 살펴보면 이를 알 수 있다. equals 에서 언급한 반사성, 대칭성, 추이성을 만족해야 한다. 또한 equals 와 마찬가지로 규약을 만족하면서 클래스를 계승하여 새로운 값 컴포넌트를 추가할 수 없기 때문에 이를 만족하려면 복합하여 view 메소드를 제공해야 한다.
Comparable 은 제네릭 인터페이스이기 때문에 컴파일 시간에 자료형이 결정된다. equals 에서는 인자의 자료형이 다른 경우를 고려하여 메소드를 작성했지만 compareTo 메소드는 다른 자료형에 대해 ClassCastException 을 발생시키면 된다.
만약 클래스에 선언된 중요 Field 가 여러 개이면 가장 중요한 Field 를 가장 먼저 비교해야 한다. 이 Field 가 같다면 그 다음 중요한 Field 를, 모든 Field 가 같다면 0을 반환한다.
public class PhoneNumberComparable implements Comparable<PhoneNumberComparable>{
private final short areaCode;
private final short prefix;
private final short lineNumber;
............
@Override
public int compareTo(PhoneNumberComparable o) {
if(this.areaCode > o.areaCode) {
return 1;
}
else if(this.areaCode < o.areaCode) {
return -1;
}
if(this.prefix > o.prefix) {
return 1;
}
else if(this.prefix < o.prefix) {
return -1;
}
if(this.lineNumber > o.lineNumber) {
return 1;
}
else if(this.lineNumber < o.lineNumber) {
return -1;
}
return 0;
}
위의 코드는 hashCode 에서 살펴본 PhoneNumber 클래스에 대해 Comparable 인터페이스를 구현한것이다. 비즈니스 로직에서 중요하다고 생각한 순서대로 비교하고 있다. 위의 메소드를 좀 더 개선하면 크기에 대한 정보도 얻을 수 있다.
@Override
public int compareTo(PhoneNumberComparable o) {
int areaDiff = this.areaCode - o.areaCode;
if(areaDiff != 0) {
return areaDiff;
}
int prefixDiff = this.prefix - o.prefix;
if(prefixDiff != 0) {
return prefixDiff;
}
int lineNumberDiff = this.lineNumber - o.lineNumber;
if(lineNumberDiff != 0) {
return lineNumberDiff;
}
return 0;
}
위 처럼 구현하면 더 많은 정보가 반환되긴 하지만 이를 적용할때에는 해당 Field 들이 음수 값을 갖지 않는다는 가정이 필요하다. 만약 (최대 - 최소)가 Integer.MAX_VALUE 값을 넘어가면 양수를 반환해야 하는 값이 음수값을 반환하게 되고 이는 오류를 내지는 않으면서 field 값에 따라서 정상, 비정상 동작을 하게 되는 상황이기 때문에 이를 인지하기가 매우 힘들다.
일반 규약 중 마지막 규칙(x.compareTo(y) == 0 일 때 x.equals(y) 만족)은 강제사항은 아니라서 만족하지 않아도 정상동작은 하겠지라고 생각하 수 있지만 이를 언급하는 이유가 있다.
public static void main(String[] args) {
BigDecimal one1 = new BigDecimal("1.0");
BigDecimal one2 = new BigDecimal("1.00");
Set<BigDecimal> hashSet = new HashSet<>();
hashSet.add(one1);
hashSet.add(one2);
System.out.println("hashSet size: " + hashSet.size());
Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(one1);
treeSet.add(one2);
System.out.println("treeSet size: " + treeSet.size());
}
..........
hashSet size: 2
treeSet size: 1
결과값을 살펴보면 hashSet 과 treeSet 의 크기가 다르다. HashSet 에 원소를 추가할 시 동치성 검사는 hash 를 사용하여 one1과 one2 을 다르다고 판단한다. 하지만 TreeSet 은 implemenets NavigableSet > extends SortedSet 를 구현하고 확장하여 정렬된 컬렉션이며, 이런 정렬된 컬렉션은 동치성 검사를 compareTo 로 하기 때문에 one1 과 one2 를 같다고 판단하기 때문이다.
'Language > Java' 카테고리의 다른 글
이펙티브 자바 - Public 클래스의 Public 필드 (0) | 2021.10.26 |
---|---|
이펙티브 자바 - 클래스와 멤버 접근 권한 (0) | 2021.10.19 |
이펙티브 자바 - clone 재정의 (0) | 2021.10.11 |
이펙티브 자바 - toString 재정의 (0) | 2021.10.04 |
이펙티브 자바 - hashCode 재정의 (2) | 2021.10.02 |
댓글