JAVA/Effective Java

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

꾸준함. 2024. 1. 30. 10:52

Comparable 인터페이스의 compareTo() 메서드는 Object 클래스에서 지원하는 메서드는 아니지만 아이템 10에서 다룬 Object.equals() 메서드를 확장한 느낌입니다.

 

compareTo 메서드 규약

compareTo 메서드의 규약은 아래와 같습니다.

  • Object.equals() 메서드 규약인 반사성, 대칭성, 추이성, 그리고 일관성을 만족하면서 순서까지 비교
  • 비교당하는 주체 this가 compareTo에 전달된 객체보다 작으면 음수, 같으면 0, 크다면 양수를 반환
    • -1, 0, 1이 아니라 음수, 0, 양수로 생각해야 함

 

  • comparetTo 메서드는 Generic을 지원하기 때문에 컴파일 시점에 타입 체킹 가능
  • 웬만하면 compareTo 메서드의 결과가 0이라면 equals 메서드 결과 또한 true여야 함

 

1. Object.equals() 메서드 규약인 반사성, 대칭성, 추이성, 그리고 일관성을 만족하면서 순서까지 비교

 

각각의 규약을 간단히 복습하면 아래와 같습니다.

 

반사성

  • 반사성은 자기 자신이 같은지 판별
  • A.equals(A)가 반드시 참

 

대칭성

  • 말 그대로 대칭이 성립
  • A.equals(B)와 B.equals(A)의 결과가 일치

 

추이성

  • 만약 A.equals(B)가 true이고, B.equals(C)가 true이면, A.equals(C)도 반드시 true를 반환

 

일관성

  • x.equals(y)가 참인 결과를 반환한다면 몇 번을 재호출해도 결과는 항상 참

 

compareTo 메서드는 위 규약을 모두 만족하면서 순서까지 비교가 가능합니다.

비교당하는 주체 this가 compareTo에 전달된 객체보다 작으면 음수, 같으면 0, 크다면 양수를 반환합니다.

 

* 주의: BigDecimal이나 Integer와 같은 primitive 클래스가 compareTo 반환값으로 -1, 0, 1을 반환한다고 해서 해당 값들에 종속된 코드를 작성해서는 안되고 음수, 0, 양수라고 생각하고 코드를 작성하는 것을 권장합니다.

 

 

 

2. comparetTo 메서드는 Generic을 지원하기 때문에 컴파일 시점에 타입 체킹 가능

 

Comparable 인터페이스를 보면 제너릭 타입을 지원하는 것을 확인할 수 있습니다.

 

 

Java의 제네릭(Generic)은 컴파일 시점에 타입 체크를 수행하여 코드의 안정성을 보장하면서 불필요한 형변환을 줄일 수 있습니다.

제네릭은 클래스나 메서드를 정의할 때, 타입 매개변수를 사용하여 클래스나 메서드 내에서 사용할 데이터의 타입을 지정할 수 수 있으며 이러한 타입 매개변수는 컴파일 시에 실제 타입으로 치환되어 타입 체크가 이루어집니다.

 

3. 웬만하면 compareTo 메서드의 결과가 0이라면 equals 메서드 결과 또한 true여야 함

 

compareTo 메서드의 결과가 0이라는 것은 두 인스턴스가 논리적으로 동치라는 의미이므로 equals 메서드 결과 또한 true여야 합니다.

하지만 equals는 소수점 자리까지 비교하기 때문에 1.0과 1.00를 비교했을 때 compareTo 메서드의 결과는 참이지만 equals 메서드 결과는 거짓이 나옵니다.

 

 

 

compareTo vs equals

compareTo와 equals는 각각 순서가 있는(ordered) 컬렉션과 순서가 없는(unordered) 컬렉션에서 사용되는 메서드로, 이들 메서드의 차이는 컬렉션의 특성과 동작 방식에 기인합니다.

 

1. compareTo: 순서가 있는 컬렉션에서 사용

 

  • 순서가 있는 컬렉션(예: 배열, 리스트)에서 요소들 간의 상대적인 순서를 비교하는 데 사용
  • 메서드 시그니처는 다음과 같습니다: int compareTo(T o).
  • 반환 값은 현재 객체가 매개변수로 전달된 객체보다 작으면 음수, 같으면 0, 크면 양수를 반환

 

2. equals: 순서가 없는 컬렉션에서 사용

 

  • equals 메서드는 Object 클래스에서 상속받은 메서드로, 객체의 동등성을 비교
  • 순서가 없는 컬렉션(예: 집합, 맵)에서 객체들 간의 동등성을 판단하는 데 사용
  • 메서드 시그니처는 다음과 같습니다: boolean equals(Object obj).
  • 주로 equals 메서드를 재정의(override)하여 객체 간의 동등성을 적절하게 비교하도록 구현

 

compareTo 구현 방법

compareTo 구현 방법은 아래와 같이 크게 두 가지가 있습니다.

  • Comparable 인터페이스를 구현하고 compareTo 메서드 재정의
  • Java 8+부터 제공되는 Comparator 인터페이스가 제공하는 정적 메서드 통해 Comparator 인스턴스 생성 후 compare 메서드 사용

 

1. Comparable 인터페이스를 구현하고 compareTo 메서드 재정의

 

제목 그대로 Comparable 제너릭 인터페이스를 구현하여 compareTo 메서드를 오버라이딩하는 방법입니다.

제너릭 인터페이스이기 때문에 equals와 달리 Object가 아닌 T를 넘겨받아 타입을 신경 쓰지 않아도 된다는 것이 가장 큰 장점입니다.

순서를 비교할 때 핵심 필드가 여러 개일 경우 비교 순서가 중요한데 순서를 결정하는데 있어 우선수위가 높은 순서로 비교하고 그 값이 0이라면 다음 필드를 비교하는 방식으로 구현하면 됩니다.

이때 필드가 Integer, Short과 같은 Primitive Type일 경우 해당 타입에서 제공하는 박싱 비교자 compareTo를 사용하는 것을 권장합니다.

  • 관계 연산자 혹은 >와 <를 사용하는 방식은 거추장스럽고 오류를 유발하므로 추천하지 않음
  • 마이너스 연산자를 통해 비교할 때 정수 오버플로우 발생 가능

 

예시는 3장에서 계속 다루었던 PhoneNumber 클래스로 작성했습니다.

 

 

 

상속보다는 컴포지션

Comparable을 구현할 때는 상속 구조보다는 컴포지션 구조를 가져가는 것을 권장합니다.

간단한 예시를 통해 이유를 설명하겠습니다.

  • 상위 클래스가 Comparable 인터페이스 구현체
  • 하위 클래스에 새로운 필드가 추가되었고 하위 클래스는 해당 필드까지 고려하여 compareTo 재정의 원함
    • 하위 클래스에서도 Comparable을 구현하면 되는 것 아닌가?라고 생각이 들겠지만 이는 구조적으로 불가능
    • Java 8+부터 추가된 Comparator 인터페이스를 이용하여 익명 Comparator를 통해 비교 가능

 

 

하지만 비교하는 경우가 많을 경우 매번 익명 Comparator를 통해 비교하기 번거롭기 때문에 아래처럼 composition 구조를 채택해 두 클래스 각각 Comparable 인터페이스를 구현해 각각 compareTo 메서드를 오버라이딩하는 것을 권장합니다.

 

 

성능

저자에 의하면 Comparable 인터페이스를 구현하여 compareTo 메서드 사용 시 약간의 성능 저하가 발생했다고 합니다.

개인적으로는 유의미한 차이가 아닐 것이라고 생각하지만 만약 Comparable 인터페이스 구현체가 애플리케이션 성능의 bottleneck이 될 경우 두 번째 방법인 Comparator 인스턴스 생성 후 인스턴스가 제공하는 compare 메서드 사용하는 것을 고려해 볼 만합니다.

 

ChatGPT가 설명하는 성능 저하 이유

  • 성능 저하가 발생하는 것은 바로 클래스를 수정해야 하는 Comparable의 특성 때문에, 클래스가 변경되면 해당 클래스를 사용하는 코드 전체가 영향을 받기 때문입니다.
  • Comparator는 외부에서 제공되므로, 클래스 변경 없이도 정렬 기준을 다양하게 조절할 수 있습니다.

 

 

2.  Comparator 인스턴스 생성 후 compare 메서드 사용

 

Java 8+ 버전부터 함수형 인터페이스, 람다, 메서드 레퍼런스와 더불어 Comparator가 제공하는 기본 메서드 및 정적 메서드를 사용해서 Compartor 구현이 가능해졌습니다.

comparingInt, comparing 등과 같은 static 메서드를 통해 Comparator 인스턴스를 생성 가능하며 팩토리 메서드를 통해 Comparator를 생성했다면 default 메서드를 사용해서 메서드 체이닝이 가능합니다.

또한 static 메서드와 default 메서드의 매개변수로 람다 표현식 혹은 메서드 레퍼런스를 사용할 수 있습니다.

자바의 정적 임포트 기능을 이용해 정적 비교자 생성자 메서드들을 이름만으로 사용할 경우 1번 방법에 비해 아래처럼 코드가 깔끔해지는 것을 확인할 수 있습니다.


 

참고

이펙티브 자바
이펙티브 자바 완벽 공략 1부 - 백기선 강사님

 

 

 

 

반응형