[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