equals 메소드를 오버라이드 하는 모든 클래스에서는 반드시 hashcode 메소드도 오버라이드 해야 한다. 그렇지 않으면 Obejct.hashcode 메소드의 보편적 계약을 위반하게 된다.

자바 API 문서의 계약 사항은 다음과 같다.

  • 애플리케이션 실행 중에 같은 객체에 대해 한 번 이상 호출 되더라도 hashCode 메소드는 같은 정수를 일관성 있게 반환해야 한다.
  • equals(Object) 메소드 호출 결과 두 객체가 동일하다면, 두 객체 각각에 대해 hashCode 메소드를 호출 했을 때 같은 정수 값이 나와야 한다.
  • equals(Object) 메소드 호출 결과 두 객체가 다르다고 해서 두 객체 각각에 대해 hashCode 메소드를 호출 했을 때 반드시 다른 정수 값이 나올 필요는 없다. 그러나 같지 않은 객체들에 대해 hashCode 메소드에서 서로 다른 정수 값을 반환하면 이 메소드를 사용하는 해시 컬렉션들의 성능을 향상시킬 수 있음을 알아야 한다.

중요한 것은 hashCode의 오버라이딩에 실패했을 때 두번째 조항 즉 동일한 객체들은 같은 해시 코드 값을 가져야 한다는 것이 위배된다.

 import java.util.*;

public final class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix,
                       int lineNumber) {
        rangeCheck(areaCode,    999, "area code");
        rangeCheck(prefix,      999, "prefix");
        rangeCheck(lineNumber, 9999, "line number");
        this.areaCode  = (short) areaCode;
        this.prefix  = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    private static void rangeCheck(int arg, int max,
                                   String name) {
        if (arg < 0 || arg > max)
           throw new IllegalArgumentException(name +": " + arg);
    }

    @Override public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber)o;
        return pn.lineNumber == lineNumber
            && pn.prefix  == prefix
            && pn.areaCode  == areaCode;
    }

위 소스에서는 hashCode에 대한 오버라이딩 메소드가 없기 때문에 문제가 된다.
아래와 같이 클래스를 HashMap과 함께 사용한다고 해보자

     public static void main(String[] args) {
        Map<PhoneNumber, String> m
            = new HashMap<PhoneNumber, String>();
        m.put(new PhoneNumber(707, 867, 5309), "Jenny");
        System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
    }
}

이 시점에서 m.get(new PhoneNumber(707, 867, 5309), "Jenny");을 호출하면 "Jenny"가 반환될 것 같지만 실제로는 null이 반환된다. 여기서 두개의 PhoneNumber 인스턴스가 개입되어 있는데 하나는 HashMap에 삽입할 때 키로 사용되었고 첫번째와 동일한 값을 갖는 두번째 인스턴스는 검색 키에서 사용되었다. 즉 PhoneNumber 클래스에서 hashCode 메소드를 오버라이드 하지 않아서 두 인스턴스가 서로 다른 해시 코드 값을 갖게 된 것이다.
 하지만 모든 객체가 다 똑같은 해시 코드를 갖게 되서는 안된다. 좋은 해시 메소드는 동일하지 않은 객체들에 대해 서로 다른 해시 코드를 만든다. 바로 이것이 hashCode 계약의 세번째 조항의 의미이다.

 @Override public int hashCode() {
      int result = 17;
      result = 31 * result + areaCode;
      result = 31 * result + prefix;
      result = 31 * result + lineNumber;
      return result;
  }

이 메소드에서는 PhoneNumber 인스턴스의 세가지 중요한 필드만으로도 간단히 연산한 결과를 반환하므로 동일한 PhoneNumber 인스턴스들은 똑같은 해시 코드 값을 갖는다. 이런 형태로 구현해야 한다.

Posted by 서오석
,