(Effective Java 2/E) 111. Item 9 - Always override hashCode when you override equals

Always override hashCode when you override equals

  • 2판 제목 : equals를 재정의할 때는 반드시 hashCode도 재정의하라
  • 3판 제목 : equals를 재정의하려거든 hashCode도 재정의하라

equals를 재정의한 클래스의 많은 버그가 hashCode에서 발생한다.

hashCode 재정의는 equals의 재정의 규약에도 포함되어있는 만큼 누락될 시 HashSet이나 HashMap과 같은 컬렉션에서 문제를 일으킬 수 있다.

좀 더 자세히 본다면 아래와 같이 명세할 수 있다.

  • 애플리케이션의 실행 중 같은 객체의 hashCode를 여러 번 호출하는 경우에, equals가 사용하는 정보들이 변경되지않았다면 항상 같은 값을 반환해야 한다.
  • equals(Object) 메서드가 같다고 판정한 두 객체의 hashCode 값은 항상 같아야 한다.
  • equals(Object) 메서드가 다르다고 판단한 두 객체의 hashCode 값은 항상 다를 필요는 없다. 하지만 HashTable의 성능을 생각하는 경우 다른 값을 가지는 것이 좋다.

따라서 equals를 재정의한 클래스는 반드시 hashCode도 재정의해야한다.

에제를 살펴보자.

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
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;

public PhoneNumber(short areaCode, short prefix, short lineNum){
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999,"프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자번호");
}
private static short rangeCheck(int val, int max, String arg){
if(val < 0 || val > max){
throw new IllegalArgumentException(arg+" : "+val);
}
return (short) val;
}

@Override
public boolean equals(Object o){
if(o == this)
return true;
if(!(o instanceof PhoneNumber)){
return false;
}

PhoneNumber pn = (PhoneNumber) o;
return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
}

}

PhoneNumber 클래스를 HashMap에 넣어서 사용한다고 가정해보자.

1
2
Map<PhoneNumber, String> map = new HashMap<>();
map.put(new PhoneNumber((short) 707,(short) 867,(short) 5309), "Jenny");

이후 아래 코드를 출력하면 어떤 값이 나올까?

1
System.out.println(map.get(new PhoneNumber((short) 707,(short) 867,(short) 5309)));

기대값은 “Jenny” 겠지만 사실 null이 출력된다.

이는 PhoneNumber 클래스에 hashCode를 재정의하지 않았기 때문이다.

반대로 hashCode를 재정의하면 문제는 해결된다.

재정의하는 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private volatile int hashCode;

@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;
}

주의해야할 것은 성능을 개선하기 위해 객체의 중요 부분을 해시코드 계산 과정에서 생략하면 안된다는 것이다.