상세 컨텐츠

본문 제목

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

Development Study/이펙티브 자바

by yooputer 2023. 5. 10. 10:45

본문

이펙티브 자바 3장 Item11를 요약한 내용입니다.


hashCode를 재정의하지 않을 때의 문제점

만약 equals를 재정의하였는데 hashCode를 재정의하지 않는 경우 다음과 같은 문제점이 발생할 수 있다.

  • equals는 두 객체를 같다고 판단하였는데 두 객체의 hashCode가 다르다
  • HashMap, HashSet같은 컬렉션의 원소로 사용할 때 문제를 일으킨다

논리적으로 같은 객체는 같은 해시코드를 반환해야 하기 때문에 hashCode를 재정의할 필요가 있다.


좋은 hash 함수가 필요한 이유

논리적으로 다른 객체가 꼭 다른 해시값을 반환해야할 필요는 없다.

하지만 다른 객체가 다른 해시값을 반환하면 해시 테이블의 성능이 높아진다.

 

만약 해시테이블에 담긴 모든 객체가 하나의 해시값을 가지고 있다면 연결리스트처럼 동작하게 되고, 

평균수행시간이 O(1)에서 O(n)으로 늘어나게 된다.

 

이상적인 해시함수는 인스턴스들이 해시값의 범위 내에서 균일하게 분배해야 한다.


좋은 hashCode를 작성하는 요령

다음은 좋은 hashCode를 작성하는 간단한 요령이다.

@Override public int has;hCode(){
    int result = 0
    
    // 모든 핵심필드의 해시코드값에 대해 아래 문장 실행
    result += 31*result + 핵심필드의 해시코드 값;

    return result;
}

 

만약 핵심필드가 기본타입 이라면 해시코드는 Type.hashCode(f)이다.

참조타입 필드라면 해당 클래스의 hashCode를 호출한다.

필드값이 null이라면 0을 사용한다 

필드가 배열이고 모든 원소가 핵심 원소라면 Arrays.hashCode를 사용하고, 그렇지 않다면 핵심 원소 각각에 대한 해시코드를 구한다

 

equals 비교에 사용되지 않는 필드는 반드시 제외해야 한다.

다른 필드로부터 계산해낼 수 있는 필드는 모두 무시해도 된다.

 

result를 갱신할 때 31을 사용하는 이유는 시프트연산과 뺄셈으로 최적화할 수 있기 때문이다. 31*i는 (i << 5) - i 와 같다

 

만약 해시충돌이 더 적은 방법을 사용해야 한다면 com.google.common.hash.Hashing을 참고하자


hash 메서드의 장점과 단점

Objects 클래스는 해시코드를 만들어주는 hash 메서드를 제공한다. 

hashCode를 한줄로 작성할 수 있다는 장점이 있지만 속도는 더 느리다. 

 

입력인수를 담기위한 배열이 만들어지고, 박싱과 언박싱도 거쳐야하기 때문이다.

hash 메서드는 성능에 민감하지 않은 상황에서 사용하자.


hash값 캐싱

클래스가 불변인 경우 인스턴스가 만들어질 때 해시코드를 계산해놓고 사용하는 방식이 좋다.

만약 해시의 키로 사용되지 않는다면 hashCode가 처음 불릴 때 계산하는 지연초기화(lazy initialization) 전략을 고려하는 것이 좋다.

단, 지연초기화를 하려면 스레드 안정성을 고려해야 한다.

private int hashCode;

@Override public int hashCode(){
	int result = hashCode;
    if (result == 0){
    	해시코드 생성
        hashCode = result;
    }
    
    return result;
}

hashCode 유의점

해시코드를 생성하는 규칙을 API사용자에게 자세히 공표하지 말자. 

더 나은 해시 방식을 알아낸 경우 계산 방식을 바꿀 수 있기 때문이다.

 

관련글 더보기