CodeGym /행동 /JAVA 25 SELF /Comparable 인터페이스: 구현, compareTo

Comparable 인터페이스: 구현, compareTo

JAVA 25 SELF
레벨 29 , 레슨 2
사용 가능

1. 객체 비교의 문제

리마인드: 참조 비교 vs. 객체 내용 비교

여러분은 이미 알고 있듯이, Java에서 == 연산자는 객체에 대해 사용할 때 참조를 비교합니다 — 즉, 같은 메모리 주소를 가리키는지 확인합니다. 필드 값이 같더라도 new로 생성된 두 객체는 == 관점에서는 서로 다릅니다.

Person p1 = new Person("Sasha", 20);
Person p2 = new Person("Sasha", 20);

System.out.println(p1 == p2); // false — 메모리에서 서로 다른 객체입니다!

내용이 같은지를 알고 싶다면 equals(), hashCode()를 사용합니다. 그런데 “누가 더 나이가 많나”, “누가 더 어리나”, “사전순으로 누가 앞서나” 같은 비교는 어떻게 할까요? 예를 들어, 사용자 목록을 나이 또는 이름으로 정렬하려는 경우입니다.

객체의 정렬과 검색이 필요한 이유

사용자 목록이 있고, 나이 기준으로 정렬하고 싶다고 해봅시다:

List<Person> people = new ArrayList<>();
people.add(new Person("Vasya", 25));
people.add(new Person("Petya", 20));
people.add(new Person("Katya", 30));

// 어떻게 정렬할까?
Collections.sort(people); // 어? Java는 Person을 어떻게 비교해야 하는지 모릅니다!

컴파일러가 즉시 경고합니다: Person 클래스가 Comparable 인터페이스를 구현하지 않았다고요. Java는 우리의 생각을 읽지 못하므로, Person에 대해 “더 크다/작다”가 무엇을 의미하는지 모릅니다. 이를 가르치려면 비교 규칙을 명시적으로 정의해야 합니다.

2. Comparable 인터페이스

인터페이스 선언

Comparable 인터페이스는 Java에 “내 클래스는 서로 비교할 수 있고, 방법은 이렇다”라고 알려주는 표준 수단입니다.

public interface Comparable<T> {
    int compareTo(T o);
}

익숙한 a.compareTo(b)는 다음을 반환합니다:

  • 음수ab보다 “작다”는 뜻입니다.
  • 0 — 두 객체는 같다고 간주됩니다.
  • 양수ab보다 “크다”는 뜻입니다.

예시: Person 클래스의 compareTo 구현

나이로 비교 가능한 Person 클래스를 만들어 봅시다:

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 게터(아래 예제를 위해)
    public String getName() { return name; }
    public int getAge() { return age; }

    // compareTo 메서드 구현
    @Override
    public int compareTo(Person other) {
        // 나이 기준 오름차순 정렬
        return Integer.compare(this.age, other.age);
        // 대안: return this.age - other.age;
    }
}

중요: 내림차순으로 정렬하려면 인자의 순서를 바꾸면 됩니다: Integer.compare(other.age, this.age).

비유. compareTo는 심판과 같습니다. 누가 앞서고, 누가 뒤이고, 누가 동률인지 명확히 판단해야 하죠. 만약 모든 심판(compareTo 메서드)이 제각각 판정하면 — 혼란이 시작됩니다!

3. Comparable 사용

Comparable로 컬렉션 정렬

이제 우리 클래스가 Comparable을 구현했으니 정렬이 “바로” 동작합니다:

List<Person> people = new ArrayList<>();
people.add(new Person("Vasya", 25));
people.add(new Person("Petya", 20));
people.add(new Person("Katya", 30));

Collections.sort(people); // compareTo를 사용!

for (Person p : people) {
    System.out.println(p.getName() + " (" + p.getAge() + ")");
}
// Petya (20)
// Vasya (25)
// Katya (30)

리스트의 sort 메서드도 마찬가지입니다:

people.sort(null); // null을 넘기면 compareTo가 사용됩니다

이름으로 정렬

이름 기준으로 정렬하고 싶다면 구현을 바꾸면 됩니다:

@Override
public int compareTo(Person other) {
    return this.name.compareTo(other.name);
}

여러 필드로 정렬

가끔은 한 필드로 먼저 비교하고, 같을 때 다른 필드로 비교해야 합니다:

@Override
public int compareTo(Person other) {
    int cmp = Integer.compare(this.age, other.age);
    if (cmp != 0) return cmp;
    return this.name.compareTo(other.name);
}

4. Comparable 구현 시 모범 사례

Comparable 계약을 지키세요

  • a.compareTo(b) == 0이라면, b.compareTo(a)도 반드시 0이어야 합니다.
  • a.compareTo(b) < 0이면, b.compareTo(a)> 0이어야 합니다(그 반대도 마찬가지).
  • a.compareTo(b) == 0일 때, 가능하면 a.equals(b)true가 되도록 하세요(엄격히 필수는 아닙니다).

왜 중요할까요?
컬렉션(예: TreeSet, TreeMap)과 정렬 메서드는 계약이 깨지면 예측 불가능하게 동작할 수 있습니다. 예를 들어, 중복을 허용하지 않는 컬렉션에 “복제”가 생길 수 있습니다.

equals와 hashCode도 잊지 마세요

compareTo를 구현했다면, equalshashCode가 올바르게 구현되어 있는지도 생각해 보세요. 특히 HashSet이나 Map 같은 컬렉션에서 사용할 클래스라면 더욱 중요합니다.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Person)) return false;
    Person other = (Person) o;
    return age == other.age && Objects.equals(name, other.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

비교 대상 필드가 null일 수 있다면, 검사 없이 compareTo에서 사용하지 마세요!

필드가 null일 수 있다면 안전한 비교를 사용하세요:

@Override
public int compareTo(Person other) {
    return Objects.compare(this.name, other.name, Comparator.nullsFirst(String::compareTo));
}

정렬된 컬렉션에 이미 들어간 후에는 compareTo에 쓰이는 필드를 바꾸지 마세요

그렇게 하면 TreeSet이나 TreeMap 같은 컬렉션 내부에서 객체가 “길을 잃을” 수 있습니다.

5. 학습용 애플리케이션 확장: 사용자 정렬

1단계: 클래스 정의

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    // ... 생성자, 게터, compareTo, equals, hashCode ...
}

2단계: 사용자 추가

List<Person> people = new ArrayList<>();
people.add(new Person("Masha", 23));
people.add(new Person("Grisha", 19));
people.add(new Person("Anya", 25));

3단계: 정렬하고 출력

Collections.sort(people);

for (Person p : people) {
    System.out.println(p.getName() + " (" + p.getAge() + ")");
}

결과:

Grisha (19)
Masha (23)
Anya (25)

6. Comparable 동작 개요

┌────────────────────────────┐
│   사용자 클래스 (Person)    │
├────────────────────────────┤
│ implements Comparable      │
│   ↓                        │
│ public int compareTo(T o)  │
│   ↓                        │
│ (this < o) → -1            │
│ (this == o) → 0            │
│ (this > o) → 1             │
└────────────────────────────┘
        │
        ▼
Collections.sort(list)
        │
        ▼
   정렬이 동작합니다!

7. 유용한 포인트

Collections.sort는 어떻게 동작하나

  • 리스트에 Comparable을 구현한 객체가 들어 있으면, 정렬은 그 객체의 compareTo를 사용합니다.
  • 구현하지 않았다면 컴파일 오류가 발생합니다.
  • 표준 타입(Integer, String 등)은 이미 Comparable이 구현되어 있습니다.

비교 방법을 여러 개로 만들 수 있을까?

  • 하나의 클래스에는 Comparable을 통한 “자연 순서”는 하나만 있습니다.
  • 대체 순서가 필요하면 Comparator를 사용하세요(다음 강의).

예: 문자열의 compareTo

String a = "apple";
String b = "banana";
System.out.println(a.compareTo(b)); // 음수 — "apple" < "banana" 이기 때문

표: compareTo가 반환하는 값

비교 반환 값
this < o
< 0
this == o
0
this > o
> 0

8. Comparable 구현 시 흔한 실수

실수 №1: compareTo 계약 위반.
a.compareTo(b)0을 반환하는데 b.compareTo(a)0이 아니면, 컬렉션이 이상하게 동작할 수 있습니다. 예를 들어, TreeSet이 두 객체를 서로 다르다고 보고 둘 다 추가할 수 있습니다.

실수 №2: 초기화되지 않은(null) 필드 사용.
비교에 사용하는 필드가 null일 수 있는데 검사를 하지 않으면 NullPointerException이 발생합니다.

실수 №3: compareTo와 equals의 불일치.
compareTo는 객체가 같다(0)고 하는데 equals는 다르다(false)고 하면, 컬렉션 사용 시 버그로 이어집니다.

실수 №4: 정렬된 컬렉션에 넣은 뒤 compareTo에 참여하는 필드를 변경.
마치 이미 알파벳 순으로 줄 서 있는데 갑자기 여권의 성을 바꾸는 것과 같습니다. 컬렉션이 그 객체를 “잃어버릴” 수 있습니다.

실수 №5: -1, 0, 1만 반환함.
compareTo는 어떤 음수나 양수도 반환할 수 있으며, 반드시 -1이나 1일 필요는 없습니다. 다만 단순화를 위해 -1/0/1을 자주 사용합니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION