주니어 개발자 성장기

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

Java/이펙티브 자바

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

Junpyo Lee 2023. 9. 1. 21:33

개요

equalshashCode는 항상 같이 구현되어야 한다. IDE, Lombok, AutoValue 등 서도 같이 구현되는 것을 전제로 한다.




일반 규약

  • equals 비교에 사용하는 정보가 변경되지 않았다면 hashCode는 매번 같은 값을 리턴해야 한다. (변경되거나, 애플리케이션을 다시 실행했다면 달라질 수 있다.)
  • equals가 두 객체를 같다고 판단했다면, hashCode의 값도 같아야 한다.
  • 두 객체에 대한 equals가 다르더라도, hashCode의 값은 같을 수 있지만 해시 테이블 성능을 고려해 다른 값을 리턴하는 것이 좋다.



제약사항

  1. equals에서 사용한 필드들을 모두 사용해야 한다.
    이렇게 해야 인스턴스 간의 차이점이 hashCode에 반영되어 다른 인스턴스가 같은 해쉬코드를 가지는 것해쉬 충돌(hash collision) 을 최대한 방지할 수 있다.

구체적으로는, HashMap에 put을 하면 Key의 hashCode 값으로 hashBucket에 집어 넣는다. hashBucket안에는 LinkedList가 구현되어 있다. 따라서 hashCode의 값이 동일한 hash collision이 일어나면 탐색의 시간 복잡도가 O(n)이 되는 것이다.





구현 방법

  1. 첫 번째 핵심필드로 hashCode 값을 구하고 int result 값에 대입한다.
  2. 그 다음 부터 정해진 순서대로 핵심필드들의 hashCode 값을 구해서 기존의 값에 31을 곱한 것에 더한다.
    EX) result = 31 * result + Short.hashCode(shortVariable)
    1. 레퍼런스 타입은 그 타입의 hashCode메서드를 호출한다.
    2. Primitive 타입은 Type.hashCode(var)을 호출하면 된다.
    3. Array(배열)은 Arrays.hashCode()를 호출하면 된다.
  3. result를 반환한다.



아래는 예시코드이다.

 // 코드 11-2 전형적인 hashCode 메서드 (70쪽)
@Override 
public int hashCode() {
    int result = Short.hashCode(areaCode); // 1
    result = 31 * result + Short.hashCode(prefix); // 2
    result = 31 * result + Short.hashCode(lineNum); // 3
    return result;
}



31인 이유?

  1. 홀수
    짝수의 경우 0이 채워지면서 정보가 손실될 수 있다.
    (컴퓨터에서 변수의 표현이 내부적으로 2진수 이므로)
  2. 어떤 개발자가 사전에서 해싱을 시도했는데 31이 가장 해쉬 충돌이 적게 일어났다고 한다.



실질적인 구현

Object.hash()를 사용하는 것이 간편하다. (내부적인 구현 방식도 위 코드와 비슷하다)

 // 코드 11-3 한 줄짜리 hashCode 메서드 - 성능이 살짝 아쉽다. (71쪽)
@Override public int hashCode() {
    return Objects.hash(lineNum, prefix, areaCode);
}

하지만 성능에 민감한 경우 성능이 약간 저하될 수도 있다는 점은 감안하자. (대부분의 경우는 상관 없다.)


  • 불변 객체의 경우 미리 hashCode를 캐싱해 놓을 수 있다.
  • hashCode를 지연 초기화할 경우 쓰레드 안정성에 신경써야 한다.




정리

  • equals를 쓰려면 hashCode도 반드시 재정의하자.
  • equals에서 사용된 핵심 필드들을 hashCode를 계산할 때 모두 사용하자.
  • 간편하게 Lombok의 @EqualsAndHashCode를 사용하자.
    → Lombok을 이용할 경우 단위 테스트를 할 필요 없다는 점도 장점이다.