아이템 14. Comparable을 구현할지 고려하라.

이펙티브 자바

아이템 14. Comparable을 구현할지 고려하라.

Comparable 인터페이스의 유일무이한 메서드인 CompareTo메서드는 이번장에서 다룬 다른 메소드들과 달리 Object 메소드가 아니다.
성격은 두가지만 빼면 Object의 equals와 같다.

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

알파벳, 숫자, 연대등 순서가 명확한 클래스인 경우 Comparable을 구현하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

package com.github.sejoung.codetest.compare;

import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

// 코드 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<>();

String[] args2 = {"B","A","D"};

for (String arg : args2)
s.add(new CaseInsensitiveString(arg));

System.out.println(s);
}
}


실행결과

1
2
3
[A, B, D]

Process finished with exit code 0

위에서는 인자 하나를 비교하는 compareTo를 구현한것이다.

다음은 이자를 여러게 받아서 비교하는것을 보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

package com.github.sejoung.codetest.compare;

import java.util.Comparator;
import java.util.NavigableSet;
import java.util.Random;
import java.util.TreeSet;
import java.util.concurrent.ThreadLocalRandom;

// 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 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진수 숫자 하나를 나타낸다.
* <p>
* 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
* 앞에서부터 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;
}

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);
}
}


실행 결과

1
2
3
[017-076-6906, 288-661-2633, 398-761-3885, 415-732-8404, 507-633-6851, 580-187-1886, 626-462-0417, 877-665-6630, 887-615-3876, 894-471-4578]

Process finished with exit code 0

자바의 정적 임포트 기능을 사용해서 위에 코드를 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

package com.github.sejoung.codetest.compare;

import java.util.Comparator;
import java.util.NavigableSet;
import java.util.Random;
import java.util.TreeSet;
import java.util.concurrent.ThreadLocalRandom;

import static java.util.Comparator.comparingInt;

// 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 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진수 숫자 하나를 나타낸다.
* <p>
* 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
* 앞에서부터 0으로 채워나간다. 예컨대 가입자 번호가 123이라면
* 전화번호의 마지막 네 문자는 "0123"이 된다.
*/
@Override
public String toString() {
return String.format("%03d-%03d-%04d",
areaCode, prefix, lineNum);
}

// 코드 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);
}
}


실행결과

1
2
3
4
[047-066-6968, 075-009-3135, 216-204-7393, 315-232-0282, 329-723-8727, 600-249-9235, 612-122-8920, 804-394-3389, 825-737-4701, 993-553-3011]

Process finished with exit code 0

자바에서 순서를 가지고있는 클래스는 Comparable을 구현하도록 하고

compareTo 메소드에서 비교 연산자<와 >는 사용하지 않도록 한다.

그대신 박싱타입이 기본제공하는 compare메소드와 Comparator에서 제공해주는 비교연산자를 사용하도록하자.

참조