해당 글은 이펙티브 자바 (Effective Java 3/E - joshua bloch) 를 읽고 정리한 글입니다.
핵심
순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야한다.
compareTo 메서드에서 필드의 값을 비교할 때 <와 > 연산자는 쓰지 않는다.
그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
이유
compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있다는 것이 Object의 equals와 다른 점이다.
compareTo 메서드의 일반 규약
이 객체와 주어진 객체의 순서를 비교한다. 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0를, 크면 양의 정수를 반환한다. 잉 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.
다음 설명에서 sgn(표현식) 표기는 수학에서 말하는 부호 함수를 뜻하며, 표현식의 값이 음수,0,양수 일때 -1,0,1을 반환하도록 정의했다.
Comparable을 구현한 클래스는 모든 x,y에 대해 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))여야 한다.
Comparable을 구현한 클래스는 추이성을 보장해야 한다. 즉, (x.compareTo(y) > 0 ) && y.compareTo(z) > 0) 이면 x.compareTo(z) > 0이다.
Comparable을 구현한 클래스는 추이성을 보장해야 한다. 즉, (x.compareTo(y) > 0 && 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 메서드와 일관되지 않다 "
첫 번째 규약
- 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다는 얘기
두 번째 규약
- 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야한다.
세 번째 규약
- 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다.
compareTo 메서드에서 관계 연산제에 <, >를 사용하지 말자.
관련 코드 및 예시
객체 참조 필드가 하나뿐인 비교자
package effectivejava.chapter3.item14;
import java.util.*;
// 코드 14-1 객체 참조 필드가 하나뿐인 비교자 (90쪽)
public final class CaseInsensitiveString
implements Comparable<CaseInsensitiveString> {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
// 수정된 equals 메서드 (56쪽)
@Override public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
@Override public int hashCode() {
return s.hashCode();
}
@Override public String toString() {
return 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);
}
}
Comparable 구현 이점
- 수많은 제네릭 알고리즘과 컬렉션의 힘을 누릴 수 있다.
package effectivejava.chapter3.item14;
import java.util.*;
// Comparable 구현 시의 이점 (87쪽)
public class WordList {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
}
구현 코드
package effectivejava.chapter3.item14;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import static java.util.Comparator.*;
// 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;
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof effectivejava.chapter3.item11.PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
@Override public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
/**
* 이 전화번호의 문자열 표현을 반환한다.
* 이 문자열은 "XXX-YYY-ZZZZ" 형태의 12글자로 구성된다.
* XXX는 지역 코드, YYY는 프리픽스, ZZZZ는 가입자 번호다.
* 각각의 대문자는 10진수 숫자 하나를 나타낸다.
*
* 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
* 앞에서부터 0으로 채워나간다. 예컨대 가입자 번호가 123이라면
* 전화번호의 마지막 네 문자는 "0123"이 된다.
*/
@Override public String toString() {
return String.format("%03d-%03d-%04d",
areaCode, prefix, lineNum);
}
// // 코드 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);
}
private static PhoneNumber randomPhoneNumber() {
Random rnd = ThreadLocalRandom.current();
return new PhoneNumber((short) rnd.nextInt(1000),
(short) rnd.nextInt(1000),
(short) rnd.nextInt(10000));
}
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);
}
}