equals를 재정의할 때는 반드시 hashCode도 재정의하라

예전에 동등성과 동일성에 대해 설명한 적이 있었다.
아마도 비슷한 내용이지 않나 싶다.
그래도 책에 나와있으니 다시 한번 포스팅해보자

Object 클래스 명세에서 복사해 온 일반 규약이다.

프로그램 실행 중에 같은 객체의 hashCode를 여러 번 호출하는 경우, eqauls가 사용하는 정보들이 변경되지 않는다면, 언제나 동일한 정수가 반환되어야 하지만 프로그램이 종료 되어 다시 실행 할 경우에는 그럴필요 없다.

equals 메서드가 같다고 판정 되면 hashCode도 같아야 한다.

equals 메서드가 다르다면 hashCode는 같을 수도 다를 수도 있지만 hashtable의 성능 향상을 위해 다른게 낫다.

만약 hashcode를 재정의하지 않으면 같은 객체는 같은 해시코드 값을 가져야 한다는 규약에 위반 된다.
다음 코드를 보자

final class PhoneNumber {
  private final short areaCode;
  private final short prefix;
  private final short lineNumber;

  public PhoneNumber(int areaCode, int prefix, int lineNumber) {
    rangeCheck(areaCode, 999, "area code");
    rangeCheck(prefix, 999, "prefix");
    rangeCheck(lineNumber, 9999, "line number");
    this.areaCode = (short) areaCode;
    this.prefix = (short) prefix;
    this.lineNumber = (short) lineNumber;
  }

  private static void rangeCheck(int arg, int max, String name) {
    if (arg < 0 || arg > max) {
      throw new IllegalArgumentException(name + ":" + arg);
    }
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (!(o instanceof PhoneNumber)) {
      return false;
    }
    PhoneNumber pn = (PhoneNumber) o;
    return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
  }
}

hashCode를 구현하지 않은 객체이다.

Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");

String s = m.get(new PhoneNumber(707, 867, 5309));
System.out.println(s);

그리고 다음과 같이 해보자
우리는 Jenny를 기대 할 것이다. 하지만 기대와 달리 null이 반환된다.
두개의 PhoneNumber 객체가 사용되었음에 유의하자 하나는 삽입할때 다른 하난(논리적으로 동일한) 꺼낼 때 사용한 객체다.
hashCode를 재정의 하지 않았기에 다른 해시코드를 갖는다. hashCode 규약에 위반 된다.
설사 운이 좋아서 같은 해시 버깃을 뒤지게 되더라고 거의 항상 null을 반환할 것이다. HashMap은 성능 최적화를 위해 내부에 보관된 항목의 해시 코드를 캐시해 두고 캐시된 해시 코드가 없으면 객체는 동일성 검사조차 하지 않는다.
이 문제는 hashCode를 구현하면 간단하게 해결 된다.

@Override
public int hashCode() {
  return 42;
}

위 메서드는 어떻게 보면 문제는 없다. 그러나 모든 객체가 같은 해시 코드를 가지게 되니 끔찍하다. 전부 같은 버킷에 해시되므로 해시 테이블은 결국 연결 리스트가 되어버린다.
복잡도가 제곱에 비례하게 증가 하면서 끔찍하게 느려진다. 해시 테이블에 저장되는 자료가 많을 경우에는 프로그램이 동작하지 않는 것처럼 되어버린다.
(아무도 느려지는 이유는 해시코드가 다르면 equals를 비교 조차 하지 않는데 매번 비교해야 하므로?)

지침의 설명은 책 혹은 인터넷에 찾아 보도록 하고 구현을 해보자

@Override
public int hashCode() {
  int result = 17;
  result = 31 * result + areaCode;
  result = 31 * result + prefix;
  result = 31 * result + lineNumber;
  return result;
}

위와 같은 메서드에서 PhoneNumber 객체의 중요한 필드만 입력으로 사용하여 결정적 계산을 수행하므로 동치 관계의 PhoneNumber 객체들에는 동일한 해시 코드를 반환한다.
result 는 0이 아닌 수면 된다. (여기서 17인 이유는 소수라서?, 31도 마찬가지?)
이렇게 구현하면 된다.
주의할 것은 성능을 개선하려고 객체의 중요 부분을 해시 코드 계산 과정에 생략 하면 절대 안된다.

요즘은 툴 혹은 lombok 에서 지원을 잘해주기 때문에 이런 구현은 극히 드물다. 물론 구현 할때도 있지 않나 싶기도 하다.
필자도 거의 구현해 본적이 없다.
툴에서 혹은 lombok에서 구현해주는 그대로라도 쓰자!