본문 바로가기
책 정리/이펙티브 자바

[이펙티브 자바] equals를 재정의하려거든 hashCode도 재정의하라 - item 11

by chanwoodev 2023. 6. 29.

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

 

hashCode의 일반 규약을 어긴다면 HashMap, HashSet 등 컬렉션에서 문제를 일으킬 수 있다.

 

Object class에 명세된 규약

  • equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다. 단, 애플리케이션을 다시 실행한다면 이 값이 달라져도 상관없다.
  • equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야한다.
  • equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.

 

논리적으로 같은 객체는 같은 해시코드를 반환해야한다 (중요)

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

위와 같은 코드에서 m.get(new PhoneNumber(707, 867, 5309)) 를 실행하면 null이 반환된다

PhoneNumber class에 hashCode를 Overriding하지 않았다면 논리적 동치일 때도 같은 hash code를 반환하지 않는다 

 

hash code 구현

 

  • 이상적인 해시 함수는 서로 다른 인스턴스들을 32비트 정수 범위에 균일하게 분배해야한다.
  • hashCode를 다 구현했다면 논리적 동치인 인스턴스에 대해 똑같은 해시 코드를 반환하는지 단위테스트를 작성한다.
  • 파생 필드는 해시코드 계산에서 제외해도 된다.
  • equals 비교에 사용되지 않는 필드반드시 제외한다.

 

해시 코드 메서드 구성

 

보통 hashcode 값을 31 * hashcode + 필드 형식으로 코드를 구성한다

31을 필드마다 곱해줌으로써 순서가 다른 경우에 대한 해시 충돌을 줄이고, 홀수이자 소수를 곱해 전반적인 해시 충돌을 줄인다

 

ArrayList의 hascode 구현부 예시

//ArrayList class 구현부
int hashCodeRange(int from, int to) {
    final Object[] es = elementData;
    if (to > es.length) {
        throw new ConcurrentModificationException();
    }
    int hashCode = 1;
    for (int i = from; i < to; i++) {
        Object e = es[i];
        hashCode = 31 * hashCode + (e == null ? 0 : e.hashCode());
    }
    return hashCode;
}

 

 

위 방식에서도 같은 예시를 확인할 수 있다

 

Objects의 hash함수를 이용할 수 있다 (성능 문제가 있음)

@Override
public int hashCode() {
    return Objects.hash(lineNum, prefix, areaCode);
}

간편하지만 입력 인수를 답기 위한 배열을 생성하고 입력 중 기본 타입의 박싱, 언박싱 과정을 거치기 떄문에 성능상 민감한 경우만 사용하는게 좋다.

 

해시코드를 캐싱하자

이 타입의 객체가 주로 해시의 키로 사용될 것 같다면 해시코드를 캐싱해두는 것이 좋다

지연 초기회를 하는경우 Thread-safe를 고려하자

 

 

hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말자

  • 클라이언트가 이 값에 의지하지 않는다.
  • 추후에 계산 방식을 바꿀 수 있다.