티스토리 뷰
※ 이 포스팅은 주관적 해석을 포함하고 있습니다.
compareTo() 구현 명세
자바 API 문서 Comparable compareTo() 구현 명세에 다음과 같은 지침이 있다.
- It is strongly recommended, but not strictly required that(x.compareTo(y)==0) == (x.equals(y)).
compareTo()는 객체 간의 자연적 순서(natural order)를 정하기 위해서 주로 사용되고, equals()는 객체 간의 동치성을 비교하기 위해서 사용된다. 사용목적이 달라 보이는 두 메서드 간에 왜 위와 같은 구현 지침이 있는 걸까?
결론부터 말하자면 (일부) Set이나 Map 메서드의 동치성 확인은 equals()가 아닌 compreTo() 메서드가 수행하기 때문이다.
명세를 지키지 않아도 되는 경우
만약 객체가 List에서만 사용되거나 equals()로만 동치성을 확인한다고 확신하는 경우 위의 명세를 준수하지 않아도 좋다.
예를 들어 Person이라는 객체는 name과 score를 갖고 있고, score의 내림차순으로 자연적 순서(natural order)를 갖는다. 그리고 Person의 동치성을 확인할 때는 오로지 name만으로 비교한다.
class Person implements Comparable<Person> {
String name;
int score;
. . . .
@Override
public int compareTo(Person o) {
return Integer.compare(o.score, score);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return name.equals(person.name);
}
. . . .
}
Comparable.compareTo() 메서드를 재정의하였기 때문에 Collections.sort() 메서드로 List를 정렬시킬 수 있다. List는 의도한 대로 정렬될 것이고 사용상의 문제가 전혀 없다.
public static void main(String[] args) {
List<Person> personList = new ArrayList<>();
personList.add(new Person("puru", 100));
personList.add(new Person("beko", 70));
personList.add(new Person("dongdong", 100));
Collections.sort(personList);
//puru, dongdong, beko 순으로 정렬
}
name으로 동치성을 확인하기 때문에 아래와 같이 HashSet에 담는 것도 가능하다.
public static void main(String[] args) {
HashSet<Person> personSet = new HashSet<>();
personSet.add(new Person("puru", 100));
personSet.add(new Person("beko", 70));
personSet.add(new Person("dongdong", 100));
//personSet의 사이즈는 3이 된다.
}
명세를 반드시 지켜야만 하는 경우
만약 compareTo()로 동치성을 확인하는 컬렉션을 사용하는 경우 반드시 위의 명세를 지켜주어야 한다.
public static void main(String[] args) {
TreeSet<Person> personSet = new TreeSet<>();
personSet.add(new Person("puru", 100));
personSet.add(new Person("beko", 70));
personSet.add(new Person("dongdong", 100));
//같은 점수를 갖는 객체가 존재하므로 size가 2가 된다.
}
TreeSet, TreeMap과 같은 컬렉션은 compareTo()를 사용하여 동치성을 확인한다. 위와 같이 compareTo()를 구현한다면 같은 score를 갖는 객체들은 같은 객체로 판단되어 TreeSet에 객체들을 의도한 대로 담기 어렵다. 그렇기 때문에 compareTo()로 비교하여 결과값이 0이 되었다면 equals()의 결과값이 true를 만족해야 한다. (x.compareTo(y) == 0) == (x.equals(y)).
명세를 지켜서 compareTo() 메서드를 구현한다면 아래와 같이 수정되어야 한다.
class Person implements Comparable<Person> {
. . . .
@Override
public int compareTo(Person o) {
if (o.score == score) {
return name.compareTo(o.name);
}
return Integer.compare(o.score, score);
}
}
score가 동률인 경우에 name으로 다시 한번 순서를 비교하게 된다면, equals()로 동치성을 확인하는 HashSet과 compareTo()로 동치성을 확인하는 TreeSet은 같은 행동을 기대할 수 있게 된다.
그럼 x.equals(y)의 결과가 true면 x.compareTo(y) == 0을 만족해야 할까?
이게 무슨 말장난인가 싶지만 한번 천천히 생각해봐야할 문제가 있다.
구현 명세는 (x.compareTo(y)==0) == (x.equals(y)) 를 준수하라고 되어있다. 수학적인 관점에서 A == B이면 B == A 역시 참이다. 그러면 (x.compareTo(y)==0) == (x.equals(y)를 준수한다면 (x.equals(y)) == (x.compareTo(y)==0) 역시 준수해야 하는 것일까? 풀어서 말하자면 x.compareTo(y)의 결과가 0인 경우 x.equals(y)가 true가 나와야 한다면, 이에 역인 x.equals(y)가 true인 경우 x.compareTo(y)의 결과가 0을 만족해야 하는가에 대한 관점이다.
개인적으로 공식문서에 저런 표현방식이 맘에 들지 않는다. (x.compareTo(y)==0) == (x.equals(y))와 같은 수식과 함께 좀 더 말로써 풀어서 설명했어야 된다고 본다.
결론을 말하자면 x.equals(y)가 true인 경우 x.compareTo(y) == 0을 만족할 필요 없다. 위의 Person 객체를 생각해보면 name이 같더라도 다른 score를 갖는 객체가 존재할 수 있기 때문이다.
2가지 생각에 기반해서 위와같은 결론을 냈다.
첫 번째, (x.compareTo(y)==0) == (x.equals(y)) 구현 명세의 '목적'을 생각해보면 된다. TreeSet과 TreeMap에서 동치성을 확인해보는 과정에 compareTo()가 사용되고 equals()로 비교하는 동치성에 부합하기 위해서 구현 명세가 만들어졌기 때문이다.
두 번째, equals()의 구현 명세에서는 compareTo()를 대비하라는 명세가 없기 때문이다. 동치성을 확인하기 위한 진짜 메서드는 equals()이고 compareTo()를 사용한 동치성 확인은 일부 특수한 경우에서만 사용되기 때문이다.
그러므로 x.equals(y)의 결과가 true라도 x.compareTo(y)의 결과가 0이 아니어도 좋다.
재밌는 건 결과를 말하고 나니 한 가지 모순이 생긴다. TreeSet과 HashSet의 동치성 결과가 온전히 동일하게 되지 않기 때문이다. 사실 이 부분은 그 객체에 대한 거시적인 설계를 생각해 봐야 하는데, 만약 Person이라는 객체를 다룰 때 name으로 동치성을 판단한다고 의도했다면 name이 같지만 score가 다른 별개의 Person이 2개 생긴다는 것이 좀 이상하다. Person 객체에 기대하는 동치성이 과연 name만으로 적절한지, 혹은 같은 name을 가진 Person이 2개 생기는 상황이 적절하지에 대해서 고민이 필요할 것이다.
참고
- https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Comparable.html#compareTo(T)
- https://stackoverflow.com/questions/62477034/is-it-possible-that-treeset-equals-hashset-but-not-hashset-equals-treeset
- https://stackoverflow.com/questions/7229977/why-it-is-implied-that-objects-are-equal-if-compareto-returns-0
'Java' 카테고리의 다른 글
IntelliJ로 JUnit4 테스트 JUnit5로 변환하기 (2) | 2020.07.01 |
---|---|
String은 항상 StringBuilder로 변환될까? (4) | 2020.04.01 |
EnumSet이 new 연산자를 사용하지 않는 이유 (0) | 2019.09.12 |
Set은 왜 get() 메소드가 없을까? (0) | 2019.07.09 |
EnumMap(EnumSet) 쓰면 좋을까? (vs HashMap) (2) | 2019.06.06 |