728x90
14. Comparable을 구현할지 고려하자.
Comparable
public interface Comparable<T> {
int compareTo(T t);
}
compareTo
는Object
의 메서드가 아니다.compareTo
는 단순 동치성 비교에 더해 순서까지 비교할 수 있으며, 제네릭하다.- 자연적인 순서 (논리적으로 자연스러운 순서) 를 정할 수 있다.
- 검색, 극단값 계산, 자동 정렬되는 컬렉션 관리도 쉽게 할 수 있다.
- 배열이라면
Arrays.sort()
를 이용해 손쉽게 정렬할 수 있다.
- 배열이라면
- 사실상 자바 플랫폼 라이브러리의 모든 값 클래스와 열거 타입이
Comparable
을 구현했다.- 정렬된 컬렉션인
TreeSet
과TreeMap
, 검색과 정렬 알고리즘을 사용하는 유틸리티 클래스인Collections
와Arrays
- 정렬된 컬렉션인
- 알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자.
Comparable은 제네릭 인터페이스이다.
compareTo
메서드의 인수 타입은 컴파일 타임에 정해진다.- 입력 인수의 타입을 확인하거나 형변환할 필요가 없다.
null
을 인수로 넣어 호출하면NPE
를 던지면 된다.
compareTo 메서드의 일반 규약
- 해당 객체와 주어진 객체의 순서를 비교한다.
- 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다.
- 객체와 비교할 수 없는 타입의 객체가 주어지면
ClassCastException
을 던진다.
- 대칭성 :
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))
- 두 객체 참조의 순서를 바꿔 비교해도 예상한 결과가 나와야 한다.
- 추이성 :
(x.compareTo(y) >0 && y.compareTo(z) >0)이면 x.compareTo(z) >0
이다.- 첫번째가 두번째보다 크고 두번째가 세번째보다 크면, 첫번째는 세번째보다 커야한다.
- 반사성 :
x.compareTo(y)==0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z))
다.- 크기가 같은 객체들끼리는 어떤 객체와 비교하더라도 항상 같아야한다.
x.compareTo(y)==0 이면 (x.equals(y))
여야한다. (필수는 아니지만 꼭 지키는 것이 좋다.)- compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다.
compareTo
로 줄지은 순서와equals
의 결과가 일관되게 한다.- 정렬된 컬렉션은 동치성을 비교할때 compareTo를 사용하기 때문이다.
- compareTo 메서드로 수행한 동치성 테스트의 결과가 equals와 같아야 한다.
상속 대신 컴포지션을 사용하자.
Comparable
을 구현한 클래스를 확장해값 컴포넌트
를 추가하고 싶다면, 확장하는 대신 독립된 클래스를 만들자.- 기존 클래스의
compareTo
(super.compareTo()
)를 사용할 때 충돌이 발생할 수 있기 때문이다. - 이 클래스에 원래 클래스의 인스턴스를 가리키는 필드를 두자.
- 내부 인스턴스를 반환하는 뷰 메서드를 제공하면 된다.
- 기존 클래스의
- 상위 클래스
public class Point implements Comparable<Point> {
final int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public int compareTo(Point p) {
int result = Integer.compare(x, p.x);
if (result == 0) {
result = Integer.compare(y, p.y);
}
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return 31 * x + y;
}
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
- 하위 클래스
public class ColorPoint implements Comparable<ColorPoint> {
private final Point point; // 컴포지션
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
// 뷰 메서드
public Point asPoint() {
return point;
}
@Override
public int compareTo(ColorPoint cp) {
int result = point.compareTo(cp.point);
if (result == 0) { // 같을 경우에 color 필드 비교
result = color.compareTo(cp.color);
}
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
@Override
public int hashCode() {
return 31 * point.hashCode() + color.hashCode();
}
@Override
public String toString() {
return point + " in " + color;
}
}
equals와 방법이 같다.
반사성, 대칭성, 추이성을 모두 충족한다.
compareTo
는 필드의 동치가 아닌 순서를 비교한다.
public class Person implements Comparable<Person> {
private final String firstName;
private final String lastName;
private final int age;
@Override
public int compareTo(Person other) {
// 1. lastName으로 먼저 비교
int result = lastName.compareTo(other.lastName);
if (result != 0) return result;
// 2. lastName이 같으면 firstName 비교
result = firstName.compareTo(other.firstName);
if (result != 0) return result;
// 3. 이름이 모두 같으면 나이 비교
return Integer.compare(age, other.age);
}
}
- 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출한다.
// 자바가 제공하는 비교자를 사용해 클래스를 비교한다.
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s); // Comparator<String>
}
Comparable
을 구현하지 않은 필드이거나 표준이 아닌 순서로 비교해야한다면Comparator
를 사용하자.Comparator
는 직접 만들거나java
가 제공하는 것을 사용하면 된다.- 정수 기본 타입이든 실수 기본 타입이든 관계 연산자인
<
,>
을 사용하지말고compare
을 이용하자. - ex)
return Integer.compare(value, other.value);
비교시 가장 핵심적인 필드부터 비교해나가자.
- 비교 결과가 0이 아니라면(순서가 결정되면) 비교가 끝나기 때문이다.
// 코드 14-2 기본 타입 필드가 여럿일 때의 비교자 (91쪽)
public int compareTo(PhoneNumber pn) {
int result = Short.compare(lineNum, pn.lineNum);
if (result == 0) {
result = Short.compare(prefix, pn.prefix);
if (result == 0)
result = Short.compare(areaCode, pn.areaCode);
}
return result;
}
비교자 생성 메서드를 사용하자.
- 기본 타입용 메서드
Comparator.comparingInt(Student::getAge)
- `Comparator.comparingDouble(Student::getGpa)
Comparator.comparingLong(Student::getId)
- 객체 참조용 메서드
comparing()
: 객체의 필드를 기준으로 비교thenComparing(
): 추가 비교 기준 지정
// 코드 14-3 비교자 생성 메서드를 활용한 비교자 (92쪽)
private static final Comparator<PhoneNumber> COMPARATOR =
comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
Comparator<Person> fullComparator = Comparator
.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparing(Person::getAge)
.thenComparing(Person::getHeight, (h1, h2) -> Double.compare(h2, h1)); // 키 역순
- 순서 변경 메서드
reversed()
: 비교 순서를 역으로naturalOrder()
: 자연적 순서reverseOrder()
: 자연적 순서의 역순
reversed( ) vs reverseOrder()
reversed()
: 전체 비교 로직 정렬 순서 뒤집기reversedOrder()
: 해당 비교의 순서만 반대로 된다.
class Student {
private int age;
private String name;
private double grade;
public Student(int age, String name, double grade) {
this.age = age;
this.name = name;
this.grade = grade;
}
public int getAge() { return age; }
public String getName() { return name; }
public double getGrade() { return grade; }
@Override
public String toString() {
return "Student{age=" + age + ", name='" + name + "', grade=" + grade + '}';
}
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student(20, "Kim", 3.5),
new Student(20, "Lee", 3.7),
new Student(19, "Park", 3.2),
new Student(20, "Choi", 3.6)
);
// 방법 1: reversed() 사용
Comparator<Student> comparator1 = Comparator
.comparing(Student::getAge)
.thenComparing(Student::getName)
.reversed() // 전체 비교 로직을 뒤집음
.thenComparing(Student::getGrade);
System.out.println("방법 1: reversed() 사용 (전체 비교 로직 뒤집기)");
students.stream()
.sorted(comparator1)
.forEach(System.out::println);
// 방법 2: 각 필드별로 reverseOrder() 사용
Comparator<Student> comparator2 = Comparator
.comparing(Student::getAge)
.thenComparing(Student::getName, Comparator.reverseOrder())
.thenComparing(Student::getGrade);
System.out.println("\n방법 2: reverseOrder() 사용 (각 필드 개별 역순)");
students.stream()
.sorted(comparator2)
.forEach(System.out::println);
}
}
방법 1: reversed() 사용 (전체 비교 로직 뒤집기)
Student{age=20, name='Lee', grade=3.7}
Student{age=20, name='Kim', grade=3.5}
Student{age=20, name='Choi', grade=3.6}
Student{age=19, name='Park', grade=3.2}
방법 2: reverseOrder() 사용 (각 필드 개별 역순)
Student{age=19, name='Park', grade=3.2}
Student{age=20, name='Lee', grade=3.7}
Student{age=20, name='Kim', grade=3.5}
Student{age=20, name='Choi', grade=3.6}
값의 차를 기준으로 비교할 때 -
를 사용하지 말자.
-
을 사용할 경우 정수 오버플로우를 일으키거나 부동소수점 계산 방식에 따른 오류를 낼 수 있다.- 정적 compare 메서드를 활용한 비교자를 사용하자.
// 코드 14-5 정적 compare 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return Integer.compare(o1.hashCode(), o2.hashCode());
}
};
- 또는 비교자 생성 메서드를 활용한 비교자를 사용하자.
// 코드 14-6 비교자 생성 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder =
Comparator.comparingInt(o -> o.hashCode());
결론
- 순서를 고려해야 하는 값 클래스를 작성한다면 꼭
Comparable
인터페이스를 구현하자.- 쉽게 정렬하고 검색하고 비교 기능을 제공하는 컬렉션과 어우러질 수 있다.
compareTo
메서드에서 필드의 값을 비교할 때<
나>
연산자는 사용하지 말자.- 대신, 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
728x90
'Java > effective java' 카테고리의 다른 글
[Effective Java] Item 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라. (0) | 2025.02.05 |
---|---|
[Effective Java] Item 15. 클래스와 멤버의 접근 권한을 최소화하라. (0) | 2025.02.05 |
[Effective Java] Item 13. clone 재정의는 주의해서 진행하라 (1) | 2025.01.20 |
[Effective Java] Item 12. toString을 항상 재정의하라. (0) | 2025.01.20 |
[Effective Java] Item 11. equals를 재정의하려거든 hashCode도 재정의하라. (0) | 2025.01.20 |