728x90
equals를 재정의하려거든 HashCode도 재정의하라.
equals
를 재정의한 클래스 모두에서HashCode
도 재정의해야 한다.- 그렇지 않으면
HashMap
이나HashSet
같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다.
- 그렇지 않으면
HashCode란?
임의의 데이터(객체, 문자열)을 고정된 길이의 정수값으로 매핑하는 함수(해시 함수)
의 결과- 같은 내용의 객체는 동일한 해시 코드를 가진다.
- 다른 내용의 객체라도 동일한 해시 코드를 가질 수 있다. (해시 충돌)
해시 충돌을 최소화하고 고른 분포를 가지도록 설계하는 것이 좋다.
용도
해시 기반 자료구조
에서 검색 시에 객체의 동일성 비교를 빠르게 수행할 수 있도록 해준다.HashMap
,HashSet
- 데이터 무결성 검증에도 사용된다.
해시 함수(hash function)
- 입력 데이터 크기와 상관없이 항상 동일한 크기의 출력값 생성
- 같은 입력에 대해 항상 같은 출력 (결정적)
- 단방향성 (출력값으로 입력값 복원 불가)
예시
MD5, SHA-1, SHA-256, ...
HashCode 재정의 규약
equals
비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의hashCode
메서드는 몇번을 호출해도 일관되게 항상 같은 값을 반환해야 한다.equals
(Object)가 두 객체를 같다고 판단했다면, 두 객체의hashCode
는 똑같은 값을 반환해야 한다.- 논리적으로 같은 객체는 같은 해시코드를 반환해야한다.
equals
(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다.- 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
- 다른 객체에 같은 값을 반환할 경우 여러 객체가 해시테이블의 버킷 하나에 담겨 마치
연결 리스트
처럼 동작한다. (O(n)
처럼 동작한다.) - 좋은 해시 함수라면 서로 다른 인스턴스에 다른 해시코드를 반환한다.
- 다른 객체에 같은 값을 반환할 경우 여러 객체가 해시테이블의 버킷 하나에 담겨 마치
- 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
두 객체가 같다면 -> hashCode가 같다.
두 객체가 다르다면 -> hashCode가 같을 수도 있지만 다른 것이 좋다.
좋은 hashCode를 작성하는 요령
@Override
public int hashCode() {
int result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
return result;
}
int
타입의 변수result
를 선언한 후 값 c로 초기화한다. 이때 c는 해당 객체의 첫번째 핵심 필드를 단계 2.a 방식으로 계산한 해시코드다.- 해당 객체의 나머지 핵심 필드 각각에 대해 다음 작업을 수행한다.
a. 해당 필드의해시 코드
를 계산한다.- 기본 타입 필드 :
Type.hashCode(f)
- 참조 타입 필드면서 클래스의 equals가 필드의 equals를 재귀적으로 호출할 경우 필드의
hashCode
를 재귀적으로 호출 (null이라면0
) - 배열이라면 핵심 원소 각각을 별도 필드처럼 다룬다. (모든 원소가 핵심 원소라면
Arrays.hashCode
, 핵심 원소가 없다면 상수(0
))
b. 해시 코드 c로 result를 갱신한다.result = 31 * result + c
- 기본 타입 필드 :
- result를 반환한다.
파생 필드는 해시 코드 계산에서 제외해도 된다.
equals에 사용하지 않는 필드는 반드시 제외해야 한다.
Object.hash
: 임의의 개수만큼 객체를 받아 해시코드를 계산
Object 클래스
의 정적 메서드- 속도는 더 느리다.
// 코드 11-3 한 줄짜리 hashCode 메서드 - 성능이 살짝 아쉽다. (71쪽)
@Override
public int hashCode() {
return Objects.hash(lineNum, prefix, areaCode);
}
클래스가 불변이고 해시코드 계산하는 비용이 크다면, 캐싱
하는 방식을 고려해야한다.
- 해시의
키
로 사용된다면 인스턴스 생성시 해시코드를 계산해두어야한다.- 해시 자료 구조를 사용하는 경우,
HashMap
등에 객체를 넣는 즉시 해시 코드가 필요하다. (버킷 위치 결정)
- 해시 자료 구조를 사용하는 경우,
- 해시의
키
로 사용되지 않는 경우 지연 초기화 전략을 사용해도 된다.
// 해시코드를 지연 초기화하는 hashCode 메서드 - 스레드 안정성까지 고려해야 한다. (71쪽)
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
사용자에게 자세히 공표하지 말자.- 클라이언트가 이 값에 의지하지 않게 되고, 추후에 계산 방식을 바꿀 수도 있다.
결론
equals
를 재정의할때는hashCode
도 반드시 재정의해야 한다.- 재정의한
hashCode
는Object
의 API 문서에 기술된 일반 규약을 따라야 한다. - 서로 다른 인스턴스라면 되도록 해시 코드도 서로 다르게 구현해야 한다.
- 같은 인스턴스라면 해시 코드는 반드시 같아야 한다.
AutoValue
나IDE
를 통해equals
와hashcode
를 자동으로 만들 수 있다.
728x90
'Java > effective java' 카테고리의 다른 글
[Effective Java] Item 13. clone 재정의는 주의해서 진행하라 (1) | 2025.01.20 |
---|---|
[Effective Java] Item 12. toString을 항상 재정의하라. (0) | 2025.01.20 |
[Effective Java] Item 10. equals는 일반 규약을 지켜 재정의하라 (1) | 2025.01.13 |
[Effective Java] 3장. 모든 객체의 공통 메서드 - 0. 들어가기 (0) | 2025.01.12 |
[Effective Java] Item 9. try-finally보다는 try-with-resources를 사용하라. (0) | 2025.01.12 |