2 minute read

item11 equals를 재정의하려거든 hashcode도 재정의하라.

equals를 재정의한 클래스에서는 hashcode도 재정의 해야한다. 그렇지 않으면 hashcode를 사용하는 HashMap, HashSet과 같은 컬렉션의 원소로 사용될 때 문제가 발생하게 된다.

hashcode 규약

  • equals 비교에 사용되는 정보가 변경되지 않았다면, hashcode는 몇번을 호출해도 항상 같은 값을 반환한다.
  • equals 가 두 객체를 같다고 판단했다면, 두 객체의 hashcode 는 같은 값을 반환한다.
  • equals 가 두 객체를 다르다고 판단했더라도, 두 객체의 hashcode 가 다를 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시 테이블의 성능이 좋아진다.
equals 메서드는 재정의했지만, hashcode를 재정의하지 않은 경우 2번째 조건을 위반하게 된다.

new를 통해 새로운 객체를 만들 때마다 늘 다른 hashcode를 리턴하게 되므로, 논리적으로 동치인 객체가 다른 hashcode를 가지게 된다.


이와 같은 경우 hashcode를 재정의하여 같은 값을 반환하도록 해 해결이 가능하다.

hashcode 재정의

@Override  
public int hashCode() {  
  return 42;  
}
  • 위 코드는 동치인 모든 객체에서 똑같은 해시코드를 반환한다. 그러나 모든 객체에 대해 똑같은 해시코드를 반환하므로, 모든 객체가 같은 해시테이블 버킷에 담겨 마치 Linked List처럼 동작한다.
  • 평균 수행시간이 O(1)에서 O(n)으로 느려져서, 성능이 매우 나빠진다.
  • 버킷 overflow가 발생할 경우 데이터가 누락될 수도 있다.

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

@Override  
public int hashCode() {  
    int c = 31;  
    // 1. int변수 result를 선언한 후 첫번째 핵심 필드에 대한 hashcode로 초기화 한다.  
    int result = Integer.hashCode(firstNumber);  
  
    // 2. 기본타입 필드라면 Type.hashCode()를 실행한다  
    // Type은 기본타입의 Boxing 클래스이다.  
    result = c * result + Integer.hashCode(secondNumber);  
  
    // 3. 참조타입이라면 참조타입에 대한 hashcode 함수를 호출 한다.  
    // 4. 값이 null이면 0을 더해 준다.  
    result = c * result + address == null ? 0 : address.hashCode();  
  
    // 5. 필드가 배열이라면 핵심 원소를 각각 필드처럼 다룬다.  
    for (String elem : arr) {  
      result = c * result + elem == null ? 0 : elem.hashCode();  
    }  
  
    //6. 배열의 모든 원소가 핵심필드이면 Arrays.hashCode를 이용한다.  
    result = c * result + Arrays.hashCode(arr);  
  
    //7. result = 31 * result + c 형태로 초기화 하여   
    //result를 리턴한다.  
    return result;  
}
  • c.f ) 31 곱하는 이유 : 홀수이면서 소수.
    • 짝수를 곱하게 되면 시프트 연산과 같은 결과를 내어 정보를 잃게 될 가능성이 존재한다.
    • 곱셈을 시프트 연산과 뺄셈으로 대체할 수 있으며 VM에서 자동으로 최적화해준다.
    • 31 * i = (i<<5)-i

구현 방법 1 : 핵심 필드만 사용한 해싱

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

구현 방법 2 : Object.hash

@Override public int hasCode(){
	return Objects.hash(lineNum, prefix, areaCode);
}
  • Objects 클래스는 임의의 개수만큼 객체를 받아 해시코드를 계산해주는 hash() 정적 메소드를 제공해준다.
  • 배열 생성과 박싱&언박싱으로 속도가 느리므로 성능에 민감하지 않은 상황에서만 사용하자.

구현 방법 3 : 캐싱

  • 클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 캐싱하는 방식을 고려해야 하는 것이 좋다.
  • 객체가 해시의 Key 로 사용되지 않는 경우라면, hashCode 가 처음 불릴 때 계산하는 지연 초기화(lazy initialization) 방식도 좋다.
    • 필드를 지연 초기화하려면 클래스를 스레드 안전하게 만들어야 한다.
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;
}

구현 시 주의사항

  • 주의 : 성능을 높이기 위해 해시코드를 계산할 때 핵심 필드를 생략해서는 안된다.
  • hashCode 가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말아야 한다.

Leave a comment