개발 공부 기록하기/01. JAVA & Kotlin

짧은 코멘트와 함께하는 이펙티브 자바) #11 equals를 재정의하려거든 hashCode도 재정의하라

lannstark 2020. 9. 29. 10:51

짧은 코멘트

  1. equals 관련 코멘트에서 다루었던 것처럼 @EqualsAndHashCode 를 사용할 수 있다. 단, 이때 컬렉션이나 순환참조 객체 등을 @EqualsAndHashCode 대상 필드에 포함시키면 문제가 될 수 있다.
  2. IntelliJ 에서 command + N(윈도우는 아마 control + N..?) 을 눌러, equals And hashCode를 자동으로 만들어줄 수 있다.

hashCode

equals를 재정의한 클래스 모두에서는 hashCode도 재정의 해야 한다 hashCode의 규약 중 일부는 이렇다 - equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메소드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다 - equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다 (보통 hashCode를 재정의하면 이 항목이 문제가 된다) - equals가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아 진다

// 적법하지만 최악의 hashCode 구현
@Override
public int hashCode() {
  return 42;
}

좋은 hashCode를 작성하는 간단한 요령

  1. int 변수 result를 객체의 첫 번째 핵심 필드 f에 대한 hashCode로 초기화 한다 이때 #1 핵심 필드가 primitive type이면 Type.hashCode(f)를 계산하고, #2 reference type이면 해당 f.hashCode()를 호출한다. 만약 계산이 복잡해질 것 같으면 이 필드의 표준형을 만들어 그 표준형의 hashCode를 호출해도 된다. 값이 null이라면 상수(관습적으로 0)를 사용한다 #3 핵심 필드가 배열이라면 각 필드에 대해 #1 또는 #2를 반복한다. 만약 모든 필드가 핵심 필드라면 Array.hashCode를 사용할 수 있다
  2. 이후 나머지 핵심 필드에 대해서도 위에 언급된 방법대로 해시코드를 계산하여 result를 갱신한다 result = 31 * result + f.hashCode()
  3. result를 반환한다

이때 핵심 필드가 아닌 필드는 반드시 제외 해야 한다

예시

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

이 방법이면 충분히 훌륭한 해시코드가 나오게 된다. 만약 해시 충돌이 더욱 적은 방법을 사용해야 한다면 구아바의 com.google.common.hash.Hashing을 참고할 수 있다

Objects 클래스르의 Objects.hash를 사용할 수도 있다. 단 속도가 조금 느리니, 성능이 주요 고려 사항이 아닐 때 사용할만하다.

캐싱

클래스가 불변이고 해시코드를 계산하는 비용이 큰 경우 캐싱을 사용할 수 있다. 특히, 주로 key로 사용되는 객체라면 인스턴스가 만들어질 때 해시코드를 계산해두어야 한다 만약 인스턴스가 만들어질 때 해시코드를 계산하는 것이 아닌 hashCode가 처음 불릴 때 계산하는 지연 초기화 전략을 사용하려면 그 클래스를 Thread-safe 하게 만들어야 한다

hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 않는 편이 좋다. 그래야 클라이언트가 이 값에 의지하지 않게 되고, 추후에 계산 방식을 바꿀 수 있다.