Item11
equals를 재정의하려거든 hashCode도 재정의하라
1 개요
- equals를 재정의한 클래스 모두에서 hashCode도 재정의해야한다
- 그렇지 않으면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를 HashMap이나 HashSet과 같은 컬렉션의 원소로 사용하지 못함
HashMap
Index = hashcode(key) & (ArraySize – 1)
1.1 Object의 hashCode 메서드 명세 규약

-
equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다.
- 단, 애플리케이션을 다시 실행한다면 이 값이 달라져도 상관없다.
-
equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
-
equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다.
- 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
1.2 주된 문제
- hashCode 재정의를 잘못했을 때 크게 문제가 되는 조항은 두 번째다
PhoneNumber.java
- equals를 재정의 했지만 hashCode는 재정의하지 않았다
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
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;
}
}
Test
@DisplayName("equals 메서드 일반 규약 5가지 만족")
@Test
void test() {
PhoneNumber x = new PhoneNumber(707, 867, 5309);
PhoneNumber y = new PhoneNumber(707, 867, 5309);
PhoneNumber z = new PhoneNumber(707, 867, 5309);
// 반사성(reflexivity)
Assertions.assertThat(x.equals(x)).isTrue();
// 대칭성(symmetry)
Assertions.assertThat(x.equals(y)).isTrue();
Assertions.assertThat(y.equals(x)).isTrue();
// 추이성(transitivity)
Assertions.assertThat(x.equals(y)).isTrue();
Assertions.assertThat(y.equals(z)).isTrue();
Assertions.assertThat(x.equals(y)).isTrue();
// 일관성(consistency)
Assertions.assertThat(x.equals(y)).isTrue();
Assertions.assertThat(x.equals(y)).isTrue();
Assertions.assertThat(x.equals(y)).isTrue();
// non-null
Assertions.assertThat(x.equals(null)).isFalse();
}
- PhoneNumber의 equals 메서드는 일반 규약 5가지를 모두 만족한다
@DisplayName("hashCode 재정의를 잘못햇을 때")
@Test
void test2() {
PhoneNumber x = new PhoneNumber(707, 867, 5309);
PhoneNumber y = new PhoneNumber(707, 867, 5309);
Assertions.assertThat(x.equals(y)).isTrue();
Assertions.assertThat(x.hashCode()).isNotEqualTo(y.hashCode());
}
- Object 명세 규약 중 두 번째 규약을 위반
- equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
@DisplayName("hashCode 재정의를 잘못하고 클래스의 인스턴스를 HashMap의 원소로 사용할 때")
@Test
void test3() {
// given
Map<PhoneNumber, String> map = new HashMap<>();
map.put(new PhoneNumber(707, 867, 5309), "제니");
// when
String value = map.get(new PhoneNumber(707, 867, 5309));
// then
Assertions.assertThat(value).isNull();
}
- Object 명세 규약 중 두 번째 규약을 위반채로 클래스의 인스턴스를 HashMap의 원소로 사용할 때 문제점
- get 메소드는 엉뚱한 해시 버킷에 가서 객체를 찾으려 한다
- 설사 두 인스턴스를 같은 버킷에 담았더라고 get 메소드는 null을 반환하는데 HashMap은 해시코드가 다른 엔트리끼리는 동치성 비교를 시도조차 안하도록 최적화 되어있기 때문
Object의 hashCode
실용적인 이유로, Object 클래스의 hashCode 메소드는 다른 객체에 대해 각기 다른 integer 값을 리턴하도록 정의되었습니다. (일반적으로 객체의 내부 주소를 integer 값으로 변환하는 방식으로 구현되지만, 그러한 구현 기법은 Java(TM) 프로그래밍 언어에서는 필수적인 것은 아닙니다)
