아이템 9. equals를 재정의할 때는 반드시 hashCode도 재정의하라.
09. equals를 재정의할 때는 반드시 hashCode도 재정의하라.
equals 메서드를 재정의하는 클래스는 반드시 hashCode 메서드도 재정의 해야 한다.
즉, 값 객체(Value Object)는 equlas와 hashCode 메서들르 재정의 해야 한다.
그렇지 않으면 Object.hashCode의 일반규약을 어기게 되므로, HashMap, HashSet, Hashtable 같은
Hash 기반 컬렉션과 함께 사용하면 오동작 한다.
[Object Class 명세규약(JavaSE 6)]
- 응용프로그램 실행 중에 같은 객체의 hashCode를 여러 번 호출하는 경우, equals가 사용하는 정보들이 변경되지 않았다면, 언제나 동일한 정수(integer)가 반환되어야 한다. 다만, 프로그램이 종료되었다가 다시 실행되어도 같은 값이 나올 필요는 없다.
- equals(Object) 메서드가 같다고 판정한 두 객체의 hashCode값은 같아야 한다.
- equals(Object) 메서드가 다르다고 판정한 두 객체의 hashCode값은 꼭 다를 필요는 없다. 그러나, 서로 다른 hashCode값이 나오면 해시 테이블(hashtable)의 성능이 향상 될수 있다는 점은 이해하고 있어야 한다.
public final 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, 999, "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( 0 > arg || max < arg ){
throw new IllegalArgumentException( name + " : " + arg );
}
}
@Override public boolean equals( Object o ){
if( this == o ) return true;
if( false == (o instanceof PhoneNumber) ) return false;
PhoneNumber pn = (PhoneNumber)o;
return areaCode == pn.areaCode
&& prefix == pn.prefix
&& lineNumber == pn.lineNumber;
}
}
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
m.get(new PhoneNumber(707, 867, 5309)); // expect : "Jenny", but return null;
위와 같이, hashCode를 정의하지 않고, HashMap의 Key로 PhoneNumber 객체를 사용하면서,
put 할때 사용한 new PhoneNumber(707, 867, 5309)
와 get 할때 사용한 객체를 이용해
값을 가져 정확히 가져 올거란 기대를 하면 안된다.
왜냐하면, Obejct.hashCode를 사용고 있기 때문에, new 할때 마다, PhoneNumber는 새로운 hashCode를 생성하기 때문이다.
[이상적인 hashCode에 가까운 함수를 만드는 방법]
- 17(임의로 잡은 값, 초기값)과 같은 0이 아닌 상수를 result라는 이름의 int 변수에 저장한다.
- 객체안에 있는 모든 중요 필드 f에 대해서(equals 메서드가 사용하는 필드를 말함) 아래의 절차를 따른다.
- (A) 해당 필드에 대한 int 해시코드
c
를 계산한다.- 필드가 boolean이면 ( f ? 1 : 0 )을 계산한다.
- 필드가 byte, char, short, int 중 하나이면 (int)f를 계산한다.
- 필드가 long이면, (int)(f^(f »> 32))를 계산한다.
- 필드가 float이면, Float.floatToIntBits(f)를 계산한다.
- 필드가 double이면, Double.doubleToLongBits(f)를 계산하고, 그 결과 로 얻은 long값을 위의 절차 long처리에 따라 해시 코드로 변환한다.
- 필드가 필드가 객체 참조이고, equals메서드가 해당 필드의 equals 메소드를 재귀적으로 호출하는 경우에는 해당 필드의 hashCode메서드를 재귀적으로 호출하여, 해시 코드를 계산한다. 좀더 복잡한 비교가 필요한 경우에는 해당 필드의 “대표 형태(canonical representation)”를 계산한 다음, 대표 형태에 대해 hashCode를 호출한다. 필드값이 null인 경우 에는 0을 반환한다. (다른 상수를 반환할 수도 있으나, 보통 0을 사용함)
- 필드가 필드가 배열인 경우에는 배열의 각 원소가 별도 빌드인 것처럼 계산한다. 즉, 각각의 중요 원소에 대해서 방금 설명한 규칙들을 재귀적으로 적용해 해시 코드를 계산하고, (B)와 같이 결합한다. 배열 내의 모든 원소가 중요하다면 JDK 1.5부터 제공되는 Arrays.hashCode 메서드 가운데 하나를 사용하면 된다.
- (B) 위 절차 (A)에서 계산된 해시 코드
c
를 result에 다음과 같이 결합한다.
result = 31 * result + c; /* 임의의 숫자 31은 소수 이면서 홀수 이기 때문에, 사용됨 숫자 31의 좋은 점은 곱셈을 시프트와 뺄셈의 조합으로 바꾸면 더 좋은 성능을 낼수 있다. 31 * i == ((i << 5) - i) */
- (A) 해당 필드에 대한 int 해시코드
- result를 반환한다.
- hashCode구현이 끝났다면, 동치 관계에 있는 객체의 해시 코드 값이 똑같이 계산되는지 점검하라. 단위 테스트를 작성해서 생각대로 되는지 확인하라. 동치 관계의 객체인데 해시 코드 값이 서로 다르다면 원인을 알아내서 고쳐야 한다.
중복 필드(redundant field)는 해시 코드 계산 과정에서 제외해도 된다. 또한, equals 계산에 쓰이지 않는 필드는 반드시 제외 해야 한다.
해시 코드 계산 비용이 높은 변경 불가능 클래스(Immutable Class)를 만들 때는, 필요할 때마다 해시 코드를 재계산하는 대신 객체 안에 캐시해 두어야 할수도 있다.대부분의 객체가 해시 키로 사용된다면, 객체를 만들 때 해시 코드를 계산해야 된다.
// 초기화 지연 기법을 사용해 해시 코드 캐싱
private vloatile int hashCode; /* vloatile 키워드는 어떤 스레드건 가장 최신의 기록된 값을 읽도록 보장함 */
@Override public int hashCode(){
int result = hashCode;
// 캐싱된 hashCode 값이 없다면,
if( 0 == result ){
result = 17;
restul = (31 * result) + areaCode;
restul = (31 * result) + prefix;
restul = (31 * result) + lineNumber;
hashCode = resutl;
}
return result;
}