Intro::
이펙티브 자바 정리본입니다.
결론
•
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여 여러 장점을 얻을 수 있다.
◦
인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어루러진다.
•
compareTo 메서드에서 필드의 값을 비교할 때 < 와 > 연산자는 쓰지 말아라
◦
대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
Comparable 이란?
•
Comparable을 구현한 했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서가 있다는 것을 의미합니다.
compareTo
•
compareTo 메서드는 Comparable 인터페이스의 유일한 메서드입니다.
Object의 equals와 성격은 같지만, 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭합니다. 그래서 Comparable을 구현한 객체들의 배열은 다음과 같이 손쉽게 정렬 가능합니다.
Arrays.sort(a);
Java
복사
그래서 왜 쓰는 걸까?
•
Comparable을 구현한다면 이 인터페이스를 활용하는 수많은 제네릭 알고리즘과 컬렉션의 힘을 누릴 수 있습니다.
•
사실상 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이 Comparable을 구현했습니다.
알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현합시다.
public interface Comparable<T> {
int compareTo(T t);
}
Java
복사
compareTo 메서드의 일반 규약
이 객체와 주어진 객체의 순서를 비교합니다. 이객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환합니다. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던집니다.
•
두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 합니다.
◦
a == b 이면 b == a, a < b 이면 b > a, …
•
첫 번째가 두 번째보다 크고 두번째가 세번째보다 크면 첫 번째는 세 번째보다 커야합니다.
•
크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 같아야 합니다.
위의 세 규약은 compareTo 메서드로 수행하는 동치성 검사도 equals 규약과 똑갗이 반사성, 대칭성, 추이성을 충족해야 함을 뜻합니다.
권고 규약
•
compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 합니다.
◦
이를 잘 지키면 compareTo로 줄지은 순서와 equals의 결과가 일관되게 됩니다.
◦
compareRo와 equals 순서가 일관되지 않은 클래스도 여전히 동작하지만, 이 클래스의 객체를 정렬된 컬렉션에 넣으면 해당 컬렉션이 구현한 인터페이스(Collection, Set 혹은 Map)에 정의된 동작과 엇박자를 낼 것입니다.
주의사항
equals 규약과 똑같다.
•
기존 클래스를 확장한 구체 클래스에서 새로운 값 컴포넌트를 추가했다면 compareTo 규약을 지킬 방법이 없다. 이를 해결하기 위해 확장 대신 독립된 클래스를 만들고 뷰를 제공하는 메서드를 사용하면 된다.
CompareTo 메서드 작성 요령
•
Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메서드의 인수 타입은 컴파일타임에 정해집니다.
◦
입력 인수의 타입을 확인하거나 형변환할 필요가 없다는 의미
class MyString implements Comparable<String> {
private String value;
public MyString(String value) {
this.value = value;
}
@Override
public int compareTo(String other) {
return this.value.compareTo(other);
}
}
Java
복사
MyString 클래스는 Comparable<String>을 구현합니다. 따라서 compareTo 메서드는 String 타입의 인수를 받습니다. 이 메서드 안에서는 other라는 인수가 이미 String 타입이라는 것이 확정되어 있기 때문에, 이 인수를 String 타입으로 변환할 필요가 없습니다.
•
null을 인수로 넣어 호출하면 NullPointerException을 던져야 합니다.
Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교해야 한다면 비교자(Comparator)를 대신 사용합니다. 비교자를 직접 만들거나 자바가 제공하는 것 중에 골라 사용하면 됩니다.
// 코드 14-1 객체 참조 필드가 하나뿐인 비교자 (90쪽)
public final class CaseInsensitiveString
implements Comparable<CaseInsensitiveString> {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
...
// 자바가 제공하는 비교자를 사용해 클래스를 비교한다.
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
public static void main(String[] args) {
Set<CaseInsensitiveString> s = new TreeSet<>();
for (String arg : args)
s.add(new CaseInsensitiveString(arg));
System.out.println(s);
}
}
Java
복사
CompareTo 메서드에서 관계 연산자 < 와 > 를 사용하는 이전 방식은 거추장스럽고 오류를 유발하니 추천하지 않습니다.
// PhoneNumber를 비교할 수 있게 만든다. (91-92쪽)
public final class PhoneNumber implements Cloneable, Comparable<PhoneNumber> {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
// // 코드 14-2 기본 타입 필드가 여럿일 때의 비교자 (91쪽)
// 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;
// }
// 코드 14-3 비교자 생성 메서드를 활용한 비교자 (92쪽)
// 약간의 성능 저하
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);
}
public static void main(String[] args) {
NavigableSet<PhoneNumber> s = new TreeSet<PhoneNumber>();
for (int i = 0; i < 10; i++)
s.add(randomPhoneNumber());
System.out.println(s);
}
}
Java
복사
References::
이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)