춤추는 개발자

[item 11] equals를 재정의하려거든 hashCode도 재정의하라 본문

Developer's_til/Effective Java

[item 11] equals를 재정의하려거든 hashCode도 재정의하라

Heon_9u 2022. 10. 19. 19:12
728x90
반응형

 equals를 재정의한 클래스 모두에서 hashCode도 재정의해야한다. 그렇지 않으면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킨다.

 

[ Object 명세에서 발췌한 규약 ]

1. equals 비교에서 사용되는 정보가 변경되지 않았다면, 어플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다.

2. equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.

3. equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다.

 

논리적으로 같은 객체는 같은 해시코드를 반환해야 한다. 예를 들어 아래와 같은 원소를 사용한다고 가정해보자.

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

위 코드에서 get 메서드를 사용하면 "수지"가 나와야할 것 같지만 실제로 null을 반환한다. 여기서는 2개의 PhoneNumber 객체가 사용됐다. PhoneNumber 클래스는 hashCode를 재정의하지 않았기 때문에 논리적 동치인 두 객체가 서로 다른 해시코드를 반환하여 두 번째 규약을 지키지 못한다.

 

 이 문제는 hashCode 메서드만 작성해주면 해결되며, 좋은 hashCode를 작성하는 간단한 요령은 다음과 같다.

 

[ hashCode 작성 요령 ]

1. int 변수 result를 선언한 후 값 c로 초기화한다. 이때 c는 해당 객체의 첫 번째 핵심 필드를 단계 2-a 방식으로 계산한 해시코드다.

2. 해당 객체의 나머지 핵심 필드 f 각각에 대해 다음 작업을 수행한다.

  a. 해당 필드의 해시코드 c를 계산한다.

    a1. 기본 타입 필드라면, Type.hashCode(f)를 수행한다. 여기서 Type은 해당 기본 타입의 박싱 클래스다.

    a2. 참조 타입 필드면서 이 클래스의 equals 메서드가 이 필드의 equals를 재귀적으로 호출해 비교한다면, 이 필드의 hashCode를 재귀적으로 호출한다. 필드의 값이 null이면 0을 사용한다.

    a3. 필드가 배열이라면, 핵심 원소 각각을 별도 필드처럼 다룬다. 모든 원소가 핵심 원소라면 Arrays.hashCode를 사용한다.

  b. 단계 2-a에서 계산한 해시코드 c로 result를 갱신한다.

      result = 31 * result + c;

3. result를 반환한다.

 

이러한 단계를 코드로 구현하면 다음과 같다.

@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, prefix, areaCode);
}

 

 클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기보다는 캐싱하는 방식을 고려해야 한다. 이 타입의 객체가 주로 해시의 키로 사용될 것 같다면 인스턴스가 만들어질 때 해시코드를 계산해둬야 한다.

해시의 키로 사용되지 않는 경우라면 hashCode가 처음 불릴 때 계산하는 지연 초기화 전략이 있다. 대신 그 클래스를 스레드 안전하게 만들도록 신경써야 한다 (Theard-safety)

@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;
}

 

 

 

 

728x90
반응형