[Effective Java] 3 - item 11 equals를 재정의하려거든 hashcode도 재정의하라.
item11 equals를 재정의하려거든 hashcode도 재정의하라.
equals를 재정의한 클래스에서는 hashcode도 재정의 해야한다. 그렇지 않으면 hashcode를 사용하는 HashMap, HashSet과 같은 컬렉션의 원소로 사용될 때 문제가 발생하게 된다.
hashcode 규약
equals비교에 사용되는 정보가 변경되지 않았다면,hashcode는 몇번을 호출해도 항상 같은 값을 반환한다.equals가 두 객체를 같다고 판단했다면, 두 객체의hashcode는 같은 값을 반환한다.equals가 두 객체를 다르다고 판단했더라도, 두 객체의hashcode가 다를 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시 테이블의 성능이 좋아진다.
equals 메서드는 재정의했지만, hashcode를 재정의하지 않은 경우 2번째 조건을 위반하게 된다.
new를 통해 새로운 객체를 만들 때마다 늘 다른 hashcode를 리턴하게 되므로, 논리적으로 동치인 객체가 다른 hashcode를 가지게 된다.
이와 같은 경우 hashcode를 재정의하여 같은 값을 반환하도록 해 해결이 가능하다.
hashcode 재정의
@Override
public int hashCode() {
return 42;
}
- 위 코드는 동치인 모든 객체에서 똑같은 해시코드를 반환한다. 그러나 모든 객체에 대해 똑같은 해시코드를 반환하므로, 모든 객체가 같은 해시테이블 버킷에 담겨 마치 Linked List처럼 동작한다.
- 평균 수행시간이
O(1)에서O(n)으로 느려져서, 성능이 매우 나빠진다. - 버킷 overflow가 발생할 경우 데이터가 누락될 수도 있다.
좋은 해시 함수는 서로 다른 인스턴스에 대해 다른 해시코드를 반환해야한다.
@Override
public int hashCode() {
int c = 31;
// 1. int변수 result를 선언한 후 첫번째 핵심 필드에 대한 hashcode로 초기화 한다.
int result = Integer.hashCode(firstNumber);
// 2. 기본타입 필드라면 Type.hashCode()를 실행한다
// Type은 기본타입의 Boxing 클래스이다.
result = c * result + Integer.hashCode(secondNumber);
// 3. 참조타입이라면 참조타입에 대한 hashcode 함수를 호출 한다.
// 4. 값이 null이면 0을 더해 준다.
result = c * result + address == null ? 0 : address.hashCode();
// 5. 필드가 배열이라면 핵심 원소를 각각 필드처럼 다룬다.
for (String elem : arr) {
result = c * result + elem == null ? 0 : elem.hashCode();
}
//6. 배열의 모든 원소가 핵심필드이면 Arrays.hashCode를 이용한다.
result = c * result + Arrays.hashCode(arr);
//7. result = 31 * result + c 형태로 초기화 하여
//result를 리턴한다.
return result;
}
- c.f ) 31 곱하는 이유 : 홀수이면서 소수.
- 짝수를 곱하게 되면 시프트 연산과 같은 결과를 내어 정보를 잃게 될 가능성이 존재한다.
- 곱셈을 시프트 연산과 뺄셈으로 대체할 수 있으며 VM에서 자동으로 최적화해준다.
31 * i = (i<<5)-i
구현 방법 1 : 핵심 필드만 사용한 해싱
@Override public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
구현 방법 2 : Object.hash
@Override public int hasCode(){
return Objects.hash(lineNum, prefix, areaCode);
}
Objects클래스는 임의의 개수만큼 객체를 받아 해시코드를 계산해주는hash()정적 메소드를 제공해준다.- 배열 생성과 박싱&언박싱으로 속도가 느리므로 성능에 민감하지 않은 상황에서만 사용하자.
구현 방법 3 : 캐싱
- 클래스가 불변이고 해시코드를 계산하는 비용이 크다면, 캐싱하는 방식을 고려해야 하는 것이 좋다.
- 객체가 해시의
Key로 사용되지 않는 경우라면,hashCode가 처음 불릴 때 계산하는 지연 초기화(lazy initialization) 방식도 좋다.- 필드를 지연 초기화하려면 클래스를 스레드 안전하게 만들어야 한다.
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;
}
구현 시 주의사항
- 주의 : 성능을 높이기 위해 해시코드를 계산할 때 핵심 필드를 생략해서는 안된다.
hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말아야 한다.
Leave a comment