본문 바로가기
Language/Java

이펙티브 자바 - Comparable

by ocwokocw 2021. 10. 17.

- 이 글은 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 를 같다고 판단하기 때문이다.

댓글