Comparable 인터페이스에는 compareTo 메서드가 있는데, Object의 equals와 유사하기 때문에 어떤 점이 다른지를 중점으로 보면 되겠다.

compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다.

=> Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서 (natural order)가 있음을 뜻한다.

Comparable을 구현하게 되면 검색, 극단값 계산,자동 정렬되는 컬렉션 관리 등을 쉽게 할 수 있기 때문에, 자바 플랫픔 라이브러리의 모든 값 클래스와 열거 타입(아이템 34)이 Comparable을 구현하고 있다.

 

비교를 활용하는 클래스의 예로는 정렬된 컬렉션인 TreeSet과 TreeMap,

검색과 정렬 알고리즘을 활용하는 유틸리티 클래스인 Collections와 Arrays가 있다.

 

 

Comparable.compare

compareTo 메서드의 일반 규약은 equals의 규약과 비슷하다.

이 객체와 주어진 객체의 순서를 비교한다.
이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다.
이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.

다음 설명에서 sgn(표현식) 표기는 수학에서 말하는 부호 함수(signum function)를 뜻하며,
표현식의 값이 음수, 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 g 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 메서드와 일관되지 않다.”

 

모든 객체에 대해 전역 동치관계를 부여하는 equals 메서드와 달리, compareTo 는 타입이 다른 객체를 신경 쓰지 않아도 된다.

(타입이 다른 객체가 주어지면 간단히 ClassCastException을 던져도 된다. 또는 다른 타입 사이의 비교할 때에는 공통 인터페이스 사용)

 

compareTo 규약을 좀 더 자세히 살펴보자.

 

1) Comparable을 구현한 클래스는 모든 x, y에 대해 sgn(x.compareTo(y)) =- -sgn(y. compareTo(x))여야 한다 - 반사성

두 객체 참조 의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다는 의미이다.

 

 

2) Comparable을 구현한 클래스는 추이성을 보장해야 한다 - 추이성

추이성은 equals 에서도 살펴봤던 개념인데, 쉽게 말하면 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다는 뜻이다.

 

 

3) Comparable을 구현한 클래스는 모든 z에 대해 x.compareTo(y) == 0이면 sgn(x. compareTo(z)) == sgn(y.compareTo(z)) - 반사성

크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다는 의미이다.

 

위 세 규약은 equals 규약에서 봤던 반사성, 대칭성, 추이성에 대한 내용이다. 그래서 주의사항도 똑같다.

기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가 했다면 compareTo 규약을 지킬 방법이 없다.

우회법도 같다. Comparable을 구현 한 클래스를 확장해 값 컴포넌트를 추가하고 싶다면, 컴포지션을 이용한다.

 

 

4) (x.compareTo(y) == 0) == (x. equals(y))여야 한다.

compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다는 의미이다.

compareTo의 순서와 equals의 결과가 일과되지 않은 클래스도 여전히 동작은 하기 때문에 꼭 지켜야 하는 것은 아니지만,

지키지 않을 경우 이 클래스의 객체를 정렬된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스(Collection, Set, 혹은 Map)에 정의된 동 작과 엇박자를 낼 것이다.

(이 인터페이스들은 equals 메서드의 규약을 따르지만, 정렬된 컬렉션들은 동치성을 비교할 때 equals 대신 compareTo를 사용)

 

compareTo와 equals가 일관되지 않는 BigDecimal 클래스를 예로 생각해보자.

BigDecimal.equals
BigDecimal.compareTo

 

HashSet은 equals로 비교하고, TreeSet은 compareTo를 사용한다.

따라서 아래와 같이 동작하게 된다.

package com.example.sypark9646.item14;

import java.math.BigDecimal;
import java.util.HashSet;
import java.util.TreeSet;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class EqualsAndCompareToCollectionTest {

		@Test
		@DisplayName("HashSet와 TreeSet의 동작 방식을 비교한다")
		void equalsAndCompareToCollectionTest(){
				HashSet<BigDecimal> hashSet = new HashSet<>();
				TreeSet<BigDecimal> treeSet = new TreeSet<>();

				hashSet.add(new BigDecimal("1.0"));
				hashSet.add(new BigDecimal("1.00"));

				treeSet.add(new BigDecimal("1.0"));
				treeSet.add(new BigDecimal("1.00"));

				Assertions.assertEquals(2, hashSet.size()); // true
				Assertions.assertEquals(1, treeSet.size()); // true
		}
}

위 BigDecimal 1.0과 1.00은 equals 메서드로 비교 하면 서로 다르기 때문에 HashSet은 원소를 2개 갖게 된다.

하지만 compareTo 메서드로 비교하 면 두 BigDecimal 인스턴스가 똑같기 때문에 TreeSet의 원소는 1개이다.

 

Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메서드의 인수 타입은 컴파일타임에 정해진다.

  • 입력 인수의 타입을 확인하거나 형변환 필요X
  • 인수의 타입이 잘못됐다면 컴파일 자체가 X
  • null을 인수로 넣어 호출하면 NullPointerException을 던지도록 한다.

 

compareTo 메서드는 각 필드가 동치인지 비교하는 게 아니라 그 순서를 비교한다.

객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출한다. Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해 야 한다면 비교자(Comparator)를 대신 사용한다.

 

compareTo를 구현할 때에는 자바 7의 일반적으로 박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드 compare를 이용하자.

(<와 > 를 사용하는 이전 방식은 헷갈리고 오류를 유발한다.)

 

자바 8부터는 Comparator 인터페이스가 일련의 비교자 생성 메서드(comparator construction method)의 팀을 꾸려 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다. 그리고 이 비교자들을 compareTo 메서드를 구현하는 데 활용할 수 있다. (약간의 성능 저하는 있다)

class PhoneNumber {
	private int areaCode, prefix, lineNum;

	public int compareToWithCompare(PhoneNumber pn) {
		int result = Integer.compare(areaCode, pn.areaCode); // 가장 중요한 필드

		if (result == 0) {
			result = Integer.compare(prefix, pn.prefix); // 두 번째로 중요한 필드
			if (result == 0) {
				result = Integer.compare(lineNum, pn.lineNum); // 세 번째로 중요한 필드
			}
		}
		return result;

	}
    
    
	// comparinglnt는 람다 (lambda)를 인수로 받는다 - 입력 인수의 타입(PhoneNumber pn)을 명시함 주의
	private static final Comparator<PhoneNumber> COMPARATOR = Comparator.comparingInt((PhoneNumber pn) -> pn.areaCode) // 가장 중요한 필드
			.thenComparingInt(pn -> pn.prefix) // 두 번째로 중요한 필드
			.thenComparingInt(pn -> pn.lineNum); // 세 번째로 중요한 필드
	

	public int compareToWithComparator(PhoneNumber pn) {
		return COMPARATOR.compare(this, pn);
	}
}

 

이 때 주의할 점은, Comparator의 compare을 정의할 때 `값의 차`를 기준으로 사용하곤 하는데, 사용하지 않도록 하자. 

왜냐하면 정수 오버플로우를 일으키거나 IEEE 754 부동소수점 계산 방식에 따른 오류를 낼 수 있다. 따라서 위에서 정의했듯이, Integer.compare 또는 Comparator.comparingInt 를 사용하도록 하자.

static Comparator<Object> hashCodeOrder = new Comparator() { 
	public int compare(Object o1, Object o2) { 
    	return o1.hashCode() - o2.hashCode(); // 오버플로우 발생 가능
	} 
};

 

 

 

 

 

+ Recent posts