아이템14. Comparable을 구현할지 고려하라
2021-07-31 00:00:00 # Effective_Java
  • Comparable 인터페이스의 유일무이한 메서드: compareTo

  • equals와 거의 비슷하지만 딱 2가지가 다르다.

    • compareTo는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다.
    • Comparable을 구현했다는 것은 그 클래스의 인스턴스들에는 자연적인 순서가 있음을 뜻한다.
    • 그래서 정렬이 쉽다. Arrays.sort(a);
  • 컬렉션 관리도 쉽다.

1
2
3
4
5
6
7
public class WordList {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
Collections.addAll(s, args);
System.out.println(s);
}
}

compareTo 메서드의 일반 규약

  • equals와 거의 비슷하기 때문에 생략 (equals와 다른점만 확인)

  • 표현식의 값이 음수, 0, 양수일 때 -1, 0, 1을 반환하도록 정의

  • 타입이 다른 객체를 신경 쓰지 않아도 된다.

    • 타입이 다르면 간단히 ClassCastException을 던진다.
    • 다른 타입 사이의 비교도 허용한다. (공통 인터페이스를 가질 때)
  • 교재 88p 참조

  • compareTo 규약을 지키지 못하면 비교를 활용하는 클래스와 어울리지 못한다. (TreeSet, TreeMap)

  • equals와 동일하게 반사성, 대칭성, 추이성을 충족해야 한다.

일반 규약 요약

    1. 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다.
    1. 첫 번째가 두 번째보다 크고 두 번째가 세 번째보다 크면, 첫 번째는 세 번째보다 커야 한다.
    1. 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야 한다. (필수는 아니지만 지키면 좋음)

주의점

  • Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메서드의 인수 타입은 컴파일타임에 정해진다.
    • 입력 인수의 타입을 확인하거나 형변환할 필요 없음 (바로 빨간줄로 보이니까!!)
  • null을 인수에 넣으면 NullPointerException
  • 각 필드가 동치인지를 비교하는 게 아니라 그 순서를 비교한다.

비교자가 하나인 compareTo

1
2
3
4
5
public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
public int compareTo(CaseInsensitiveString cis) { // 해당 객체의 인스턴스 변수 s와 인수 객체 속 s
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
}

자바7 이후 변경 상황

  • compareTo 메서드에서 관계 연산자 <와 >를 사용하는 이전 방식은 거추장스럽고 오류를 유발하니, 이제는 추천 X

자바 8에서 객체를 비교하는 방식

  • 간결하고 성능 조금 안좋아지는 방식

    1
    2
    3
    4
    5
    6
    7
    8
    // Comparator 인터페이스가 일련의 비교자 생성 메서드와 팀을 꾸려 메서드 연쇄 방식으로 비교자를 생성할 수 있게 되었다.
    private static final Comparator<PhoneNumer> COMPARATOR = comparingInt((PhoneNumber pn) -> pn.areaCode)
    .thenComparingInt(pn -> pn.prefix) // 추가 비교, thenComparingInt 사용 시 타입 추론 가능
    .thenComparingInt(pn -> pn.lineNum); // 추가 비교

    public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
    }
    • comparingInt는 객체 참조를 int 타입 키에 매핑하는 키 추출 함수를 인수로 받아, 그 키를 기준으로 순서를 정하는 비교자를 반환하는 정적 메서드

객체 참조용 비교자 생성 메서드도 준비됨

  • comparing이라는 정적 메서드 2개가 다중정의되어 있다.

  • 첫번째 메서드: 키 추출자를 받아서 그 키의 자연적 순서를 사용한다.

  • 두번째 메서드: 키 추출자 하나와 추출된 키를 비교할 비교자까지 총 2개의 인수를 받는다.

  • 또한, thenComparing이란 인스턴스 메서드가 3개 다중정의되어 있다.

그냥 comparing을 찾아와보자

Comparator 인터페이스를 가져와봤다.

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/*
* Copyright (c) 1997, 2020, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/

package java.util;

import java.io.Serializable;
import java.util.function.Function;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.function.ToDoubleFunction;
import java.util.Comparators;

@FunctionalInterface
public interface Comparator<T> {

int compare(T o1, T o2);

boolean equals(Object obj);

default Comparator<T> reversed() {
return Collections.reverseOrder(this);
}

default Comparator<T> thenComparing(Comparator<? super T> other) {
Objects.requireNonNull(other);
return (Comparator<T> & Serializable) (c1, c2) -> {
int res = compare(c1, c2);
return (res != 0) ? res : other.compare(c1, c2);
};
}

default <U> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator)
{
return thenComparing(comparing(keyExtractor, keyComparator));
}

default <U extends Comparable<? super U>> Comparator<T> thenComparing(
Function<? super T, ? extends U> keyExtractor)
{
return thenComparing(comparing(keyExtractor));
}

default Comparator<T> thenComparingInt(ToIntFunction<? super T> keyExtractor) {
return thenComparing(comparingInt(keyExtractor));
}

default Comparator<T> thenComparingLong(ToLongFunction<? super T> keyExtractor) {
return thenComparing(comparingLong(keyExtractor));
}

default Comparator<T> thenComparingDouble(ToDoubleFunction<? super T> keyExtractor) {
return thenComparing(comparingDouble(keyExtractor));
}

public static <T extends Comparable<? super T>> Comparator<T> reverseOrder() {
return Collections.reverseOrder();
}

@SuppressWarnings("unchecked")
public static <T extends Comparable<? super T>> Comparator<T> naturalOrder() {
return (Comparator<T>) Comparators.NaturalOrderComparator.INSTANCE;
}

public static <T> Comparator<T> nullsFirst(Comparator<? super T> comparator) {
return new Comparators.NullComparator<>(true, comparator);
}

public static <T> Comparator<T> nullsLast(Comparator<? super T> comparator) {
return new Comparators.NullComparator<>(false, comparator);
}

public static <T, U> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor,
Comparator<? super U> keyComparator)
{
Objects.requireNonNull(keyExtractor);
Objects.requireNonNull(keyComparator);
return (Comparator<T> & Serializable)
(c1, c2) -> keyComparator.compare(keyExtractor.apply(c1),
keyExtractor.apply(c2));
}

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

public static <T> Comparator<T> comparingInt(ToIntFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Integer.compare(keyExtractor.applyAsInt(c1), keyExtractor.applyAsInt(c2));
}

public static <T> Comparator<T> comparingLong(ToLongFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Long.compare(keyExtractor.applyAsLong(c1), keyExtractor.applyAsLong(c2));
}

public static<T> Comparator<T> comparingDouble(ToDoubleFunction<? super T> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> Double.compare(keyExtractor.applyAsDouble(c1), keyExtractor.applyAsDouble(c2));
}
}

  • int, long, double을 위한 메소드들이 추가적으로 보인다.

핵심 정리

  • 순서를 고려해야 하는 값 클래스를 작성한다면, 꼭 Comparable 인터페이스를 구현하자
  • Comparable 인터페이스를 구현하면, 그 인스턴스들을 쉽게 정렬하고, 검색하고, 비교 기능을 제공하는 컬렉션과 어우지도록 해야 한다.
  • compareTo 메서드는 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.