본문 바로가기

도서/이펙티브 자바

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

 

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

재정의하지 않으면 hashCode 일반 규약을 어겨, HashMap, HashSet과 같은 컬렉션 원소로 사용할 때 문제가 생긴다.

 

 

⭐️ 논리적으로 같은 객체는 같은 해시코드를 반환해야한다. 

 

hashCode를 재정의하지 않은 예시

HashMap에 저장할 때 사용된 인스턴스, 조회할 때 사용된 인스턴스가 2개 사용되었다.

둘은 논리적으로 같지만, 물리적으로 다르기 때문에 다른 해시 코드를 리턴하게 된다.

Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "제니");
System.out.println(m.get(new PhoneNumber(707, 867, 5309)); // null 반환

 

 

잘못된 hasCode 구현방법

모든 객체에게 똑같은 값을 주기 때문에, 연결리스트 처럼 동작한다.

따라서 O(1)인 해시테이블이 O(n) 수준으로 느려진다. 객체가 많다면 더욱 더 느려진다.

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

 

 

이상적인 hashCode는 서로 다른 인스턴스에 다른 해시코드를 반환해야한다.

좋은 hasCode 구현방법

1. int 변수를 result로 선언하여 c값으로 초기화한다 (첫 번재 핵심 필드(equals 비교에 사용되는) 를 계산한 해시코드)

 

2. 나머지 핵심 필드 각각에 아래 작업을 수행한다.

  • 1) 해당 필드의 해시코드 c를 계산한다.
    • 기본 타입 필드: Type.hashCode(f)
    • 참조 타입 필드:
      • equals를 재귀적으로 호출하는 경우, hashCode 재귀적으로 호출
      • 계산이 복잡해지는 경우, 표준형의 hashCode 사용 
      • 필드 값이 null인 경우, 0 사용
    • 배열: 핵심 원소 각각을 별도 필드처럼 다루기
      • 모든 원소가 핵심 원소인 경우, Arrays.hashCode 사용
      • 배열에 핵심 원소가 하나도 없는 경우, 상수 사용 (0)
      • 일부 원소가 핵심인 경우, result = 31 * result +c 식 사용
  • 2) 구한 c를 바탕으로 result를 갱신한다. 
    • result = 31 * result + c
    • 31을 곱하는 이유는 홀수이면서 소수이기 때문에 (오버플로우 방지용) 
  • 3) result를 반환한다.

 

이때 아래 필드는 제외한다.

  • 파생 필드 (다른 필드로 부터 계산해내는 필드) 무시 가능
  • equals 비교에 사용되지 않은 필드는 반드시 제외

성능을 높이기 위해 해시코드 계산 시 핵심 필드를 생략하면 안된다.

속도는 빨라지지만, 해시 품질이 나빠져 테이블 성능을 떨어뜨릴 수 있다.

 

 

잘 구현된 hashCode

  • 핵심 필드만 사용해 간단한 계산 수행, 비결정적 요소 제외
@Override public int hashCode(){
	int result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix);
    result = 31 * result + Short.hashCode(lineNum);
    return result;
}

 

 

Objects 클래스에서 제공하는 hash 메서드

  • 위 코드와 다르게 속도가 느림
  • 입력 인수를 담기 위한 배열 생성, 입력 중 기본 타입이 있다면 박싱과 언박싱 과정이 필요하기 때문
  • 성능이 민감하지 않은 상황에서만 사용할 것
@Override public int hashCode(){
	return Objects.hash(lineNum, preifx, areaCode);
}

 

 

지연 초기화하는 hashCode

  • 불변 클래스에서 해시 코드 계산 비용이 크다면, 캐싱을 고려해야한다. (해시 코드를 다시 계산할 필요가 없으므로 저장해서 사용)
  • 처음 호출될 때만 계산하고, 이후에는 계산 값을 리턴하는 지연 초기화 전략을 사용하자.
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;
}

 

 

 

 

 

 

반응형