일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 제네릭 타입
- throwable
- github api
- yield
- System.err
- auto.create.topics.enable
- 제네릭 와일드 카드
- 바운디드 타입
- 로컬 클래스
- 람다식
- annotation processor
- 자바스터디
- System.in
- raw 타입
- 상속
- 항해99
- docker
- 브릿지 메소드
- junit 5
- Switch Expressions
- 프리미티브 타입
- 정렬
- 스파르타코딩클럽
- 익명 클래스
- 합병 정렬
- 자바할래
- 접근지시자
- Study Halle
- System.out
- 함수형 인터페이스
- Today
- Total
코딩하는 털보
이펙티브 자바, 아이템 11. equals를 재정의 하려거든 hashCode도 재정의하라 본문
이펙티브 자바, 아이템 11. equals를 재정의 하려거든 hashCode도 재정의하라
equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다.
그렇지 않으면 hashCode 일반 규약을 어기게 되어 HashMap 또는 HashSet 같은 컬렉션의 원소로 사용할 때 문제가 발생한다.
hashCode 규약
- equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다.
- equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
- equals가 두 객체를 다르다고 판단했다면, 두 객체의 hashCode는 다르게 반환해야 해시 테이블의 성능이 좋아진다.(필수는 아님)
HashMap 같은 경우 hashCode가 다른 객체에 대해서 동치성 비교를 하지 않고 다른 객체라고 판단한다.
아래와 같이 세 개의 번호로 논리적 동치성을 확인하는 PhoneNumber가 있다. PhoneNumber는 equals만 재정의했고 hashCode는 재정의하지 않았다.
public class PhoneNumber {
private int ariaCode, prefix, lineNum;
public PhoneNumber(int ariaCode, int prefix, int lineNum) {
this.ariaCode = ariaCode;
this.prefix = prefix;
this.lineNum = lineNum;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof PhoneNumber)) {
return false;
}
PhoneNumber that = (PhoneNumber) o;
return ariaCode == that.ariaCode && prefix == that.prefix && lineNum == that.lineNum;
}
}
hashCode를 재정의하지 않은 PhoneNumber는 규약을 위반했으며 HashMap의 원소로 사용하면 논리적 문제가 발생한다.
public static void main(String[] args) {
PhoneNumber pn1 = new PhoneNumber(123,456,7890);
PhoneNumber pn2 = new PhoneNumber(123,456,7890);
//true, 번호가 같으므로 논리적으로 동치
System.out.println(pn1.equals(pn2));
Map<PhoneNumber, String> map = new HashMap<>();
map.put(pn1, "정인");
//논리적으로 동치이니 "정인"을 반환활 것 같지만 null을 반환한다.
System.out.println(map.get(pn2));
}
이상적인 hashCode 메서드 작성 요령
//c1는 첫번째 핵심 필드의 해시코드
//필드 f가 기본 타입이면 Type.hashCode(f)
//필드 f가 참조 타입이면 필드의 f.hashCode(), 필드 값이 null이면 0
//필드 f가 배열이면 배열안있는 핵심 원소를 별도의 필드처럼 계산. 핵심 원소가 없으면 0, 모든 원소가 핵심 원소이면 Arrays.hashCode()
int result = c1;
//c2는 두번째 핵심 필드의 해시코드
//31을 곱하는 것은 해시 효과를 높여주기 위함이다.
result = 31*result+c2;
...
return result;
hashCode를 구현했다면 동치인 인스턴스에 대해 같은 hashCode를 반환하는지 자문하고 단위 테스트를 작성한다.
파생 필드나 equals 비교에 사용되지 않은 필드는 해시코드 계산에서 제외한다.
@Override
public int hashCode() {
int result = 31 * Integer.hashCode(ariaCode);
result = 31 * result+Integer.hashCode(prefix);
result = 31 * result+Integer.hashCode(lineNum);
return result;
}
다시 테스트
public static void main(String[] args) {
PhoneNumber pn1 = new PhoneNumber(123,456,7890);
PhoneNumber pn2 = new PhoneNumber(123,456,7890);
System.out.println(pn1.equals(pn2));
Map<PhoneNumber, String> map = new HashMap<>();
map.put(pn1, "정인");
//재정의한 hashCode에 의해 "정인"을 반환한다.
System.out.println(map.get(pn2));
}
인텔리제이에서 자동으로 만들어주는 hashCode는 Objects.hash()를 사용하는데, 이 메서드는 조금 느릴 수 있기 때문에 성능에 민감하지 않은 상황에서만 사용한다.
@Override
public int hashCode() {
return Objects.hash(ariaCode, prefix, lineNum);
}
만약 어떤 불변클래스 타입의 객체가 주로 해시의 키로 사용되며 해시코드 계산 비용이 클 땐,
매번 계산하기 보다는 인스턴스가 만들어질 때 해시코드를 계산해두어 캐싱하는 방식을 고려한다. (대신 객체 생성 속도가 느려지겠네용...)
성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안 된다.
핵심 필드를 생략하면 해시코드 계산을 빨라질 수도 있지만 해시 테이블 성능이 심각하게 떨어질 수 있다.
hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말자. 그래야 클라이언트가 이 값에 의지하지 않게 되고, 추후에 계산 방식을 바꿀 수도 있다.
API 사용자가 내가 만든 hashCode에 의존하게 된다면? hashCode를 수정하기 어렵겠네요!