아이템 10. equals는 일반 규약을 지켜 재정의하라

equals 메서드는 재정의하기 쉬워 보이지만 곳곳에 함정이 도사리고 있어서 자칫하면 끔찍한 결과를 초래한다.
문제를 회피하는 가장 쉬운 방법은 아예 재정의하지 않는 것이다.

재정의 하지 말아야 하는 상황들

  • 각 인스턴스가 본질적으로 고유하다. 값을 표현하는 게 아니라 동작하는 개체를 표현하는 클래스가 여기에 해당한다.(예: Thread)

  • 인스턴스의 '논리적 동치성'을 검사할 일이 없다.

  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.

  • 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다. equals가 실수로라도 호출되는 걸 막고 싶다면 아래처럼 구현하자.

    @Override public boolean equals(Object o) {
      throw new AssertionError(); // 호출 금지!
    }

재정의 해야 하는 상황들

  • 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때
    • 두 값 객체를 equals로 비교하는 프로그래머는 객체가 같은지가 아니라 값이 같은지를 알고 싶어 할 것이다.
  • 재정의할 때 반드시 따라야 하는 일반 규약들
    • 반사성, 대칭성, 추이성, 일관성, null-아님

equals 메서드 구현 방법 단계

  1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
  2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
    • 인터페이스를 구현한 클래스라면 equals에서 (클래스가 아닌) 해당 인터페이스를 사용해야 한다.
  3. 입력을 올바른 타입으로 형변환 한다. 앞서 2번 검사를 해서 100% 성공한다.
  4. 입력 객체와 자기 자신의 대응되는 '핵심'필드들이 모두 일치하는지 하나씩 검사한다.
    • 모든 필드가 일치하면 true, 하나라도 다르면 false를 반환한다.
    • 2번에서 인터페이스를 사용했다면 입력의 필드 값을 가져올 때도 그 인터페이스의 메서드를 사용해야 한다.
    • 타입별 비교 방법
      • float와 double을 제외한 기본 타입 필드: == 연산자로 비교
      • float와 double 필드: 각각 정적 메서드인 Float.compare(float, float)와 Double.compare(double, double)로 비교
        • float과 double을 특별 취급하는 이유는 Float.NaN, -0.0f, 특수한 부동소수 값 등을 다뤄야 하기 떄문이다.
        • Float.equals 메서드는 오토박싱을 수반할 수 있어서 성능상 좋지 않다.
      • 참조 타입 필드: 각각의 equals 메서드로 비교
      • 배열의 모든 원소가 핵심 필드라면 Arrays.equals 메서드들 중 하나를 사용
      • 비교하기 아주 복잡한 필드: 그 필드의 표준형(canonical form)을 저장해둔 후 표준형끼리 비교하면 훨씬 경제적이다. (불변 클래스에 제격)
    • 다를 가능성이 더 크거나 비교하는 비용이 싼 필드를 먼저 비교하자.
  5. equals를 다 구현했다면 대칭적인지, 추이성이 있는지, 일관적인지 자문해보자.

주의사항

  • equals를 재정의할 땐 hashCode도 반드시 재정의하자(아이템 11).
  • 너무 복잡하게 해결하려 들지 말자.
    • 필드들의 동치성만 검사해도 equals 규약을 어렵지 않게 지킬 수 있다. 일반적으로 별칭(alias)은 비교하지 않는 게 좋다.
  • equals 메서드는 Object 타입을 매개변수로 받도록 선언하자.

equals(hashCode도 마찬가지)를 작성하고 테스트하는 일은 지루하다. 다행이도 구글이 만듬 AutoValue 프레임워크가 있다. 클래스에 애너테이션 하나만 추가하면 AutoValue가 이 메서드들을 알아서 작성해준다.

대다수의 IDE도 같은 기능을 제공하지만 생성된 코드가 AutoValue만큼 깔끔하거나 읽기 좋지는 않다. 그래도 사람이 직접 작성하는 것보다는 IDE에 맡기는 편이 낫다.

핵심 정리

꼭 필요한 경우가 아니면 equals를 재정의하지 말자. 대부분의 경우에 Object의 equals가 원하는 비교를 정확히 수행해준다. 재정의해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약을 확실히 지켜나며 비교해야 한다.

아이템 11. equals를 재정의하려거든 hashCode도 재정의하라

equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다.

hashCode 재정의를 잘못했을 때 크게 문제가 되는 조항은 두 번째다. 즉, 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.

좋은 해시 함수라면 서로 다른 인스턴스에 다른 해시코드를 반환한다.

성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안된다.

AutoValue 프레임워크를 사용하면 멋진 equals와 hashCode를 자동으로 만들어준다.

전형적인 hashCode 메서드

@Override public int hashCode() {
    int result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix);
    result = 31 * result + Short.hashCode(lineNum);
    return result;
}

해시코드를 지연 초기화하는 hashCode 메서드 - 스레드 안전성까지 고려해야 한다.

private int hashCode; // 자동으로 0으로 초기화된다.

@Override public int hashCode() {
    int result = hashCode;
    if (result == 0) {
        result = Short.hashCode(areaCode);
        result = 31 * result + Short.hashCode(prefix);
        result = 31 * result + Short.hashCode(lineNum);
        hashCode = result;
    }
    return result;
}

아이템 12. toString을 항상 재정의하라

toString을 잘 구현한 클래스는 사용하기에 훨씬 즐겁고, 그 클래스를 사용한 시스템은 디버깅하기 쉽다.

  • toString은 그 객체가 가진 주요 정보 모두를 반환하는 게 좋다.
  • 객체가 거대하거나 객체의 상태가 문자열로 표현하기에 적합하지 않다면 무리가 있다.
    • 이런 상황에서는 "맨해튼 거주자 전화번호부(총 1487536개)"나 "Thread[main,5,main]" 같은 요약 정보를 담아야 한다.
  • 이상적으로는 스스로를 완벽히 설명하는 문자열이여야 한다. (위의 스레드 예는 이 조건에 맞지 않음)
  • toString을 구현할 때면 반환값의 포맷을 문서화할지 정해야 한다.
    • 전화번호나 행렬 같은 값 클래스는 문서화하기를 권한다.
    • 포맷을 명시하면 그 값 그대로 입출력에 사용하거나 csv 파일처럼 데이터 객체로 저장할 수도 있다.
  • 단점은 포맷을 한번 명시하면 그 포맷에 얽매이게 된다. 이를 사용하는 프로그래머들이 그 포맷에 맞춰 파싱하고, 새로운 객체를 만들고, 영속 데이터로 저장하는 코드를 작성할 것이다.
  • 반대로 포맷을 명시하지 않는다면 향후 릴리스에서 정보를 더 넣거나 포맷을 개선할 수 있는 유연성을 얻게 된다.
  • 포맷을 명시하든 아니든 의도는 명확히 밝혀야 한다.
  • 대부분의 IDE 에서는 toString을 생성해준다. 하지만 자동 생성에 적합한 클래스인지 확인하자. (전화번호는 표준 체계를 따라야 한다. 000-0000-0000)

+ 따끈한 최근 게시물