equals 비교에서 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일관되게 항상 같은 값을 반환해야 한다.
equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
hashCode 를 재정의 하지 않았을 때 발생할 수 있는 문제
HashMap 에서 키를 클래스로 사용하게 될때 의도와 다르게 동작할 수 있다.
public class NonOverrideHashCodeTest {
public static class MyKey {
private int num;
private String str;
public MyKey(int num, String str) {
this.num = num;
this.str = str;
}
}
@Test
public void hashMap() {
// given
Map<MyKey, String> map = new HashMap<>();
MyKey myKey = new MyKey(1, "a");
map.put(myKey, "myValue1");
// when
String actual = map.get(new MyKey(1, "a"));
// then
assertThat(actual, is(nullValue()));
}
@Test
public void hashCode_notEqual() {
// given
MyKey myKey1 = new MyKey(1, "a");
MyKey myKey2 = new MyKey(1, "a");
// when
int hashCode1 = myKey1.hashCode();
int hashCode2 = myKey2.hashCode();
// then
assertThat(hashCode1, not(hashCode2));
}
}
올바른 hashCode 재정의 방법
equals 에 포함된 필드는 필수적으로 hashCode 에도 포함돼야 한다.
파생 필드는 제외 가능
반대로 equals 에 포함되지 않은 필드는 hashCode 에도 포함해서는 안된다.
int 형 변수(result)를 하나 선언하고 핵심 필드(equals 에 포함된)의 hashCode 값으로 초기화
hashCode 구하기
기본 타입 필드는 <TypeClass>.hashCode() 사용
참조 타입 필드는 equals 와 동일하게 맞추는것이 중요. 혹은 구현된 hashCode 사용. null 일경우 0사용
배열의 경우 각 원소를 개별 필드로 다뤄야함. 핵심 원소만 사용하거나 Arrays.hasCode() 사용. 배열이 빈경우 0사용
계산한 해시코드(c) 는 아래 연산으로 result 에 연산 수행
result = 31 * result + c
public class MyKey {
private int i;
private String str;
private long l;
private double d;
private float f;
private short s;
private char c;
private List<Integer> integers;
private MySubKey mySubKey;
@Override
public int hashCode() {
int result;
long temp;
// int 변수를 핵심 필드중 하나의 해시코드 값으로 초기화
result = i;
// String(기본 타입) 해시코드 구하기
result = 31 * result + (str != null ? str.hashCode() : 0);
// long 해시코드 구하기
result = 31 * result + (int) (l ^ (l >>> 32));
// double 해시코드 구하기
temp = Double.doubleToLongBits(d);
result = 31 * result + (int) (temp ^ (temp >>> 32));
// float 해시코드 구하기
result = 31 * result + (f != +0.0f ? Float.floatToIntBits(f) : 0);
// short 해시코드 구하기
result = 31 * result + (int) s;
// char 해시코드 구하기
result = 31 * result + (int) c;
// List 해시코드 구하기
result = 31 * result + (integers != null ? integers.hashCode() : 0);
// 참조 타입 해시코드 구하기
result = 31 * result + (mySubKey != null ? mySubKey.hashCode() : 0);
return result;
}
}
Item 11. equals 를 재정의하려거든 hashCode 도 재정의하라.
Object hashcode 명세(equals 와 hashCode 를 꼭 함께 재정의 해야하는 이유)
Object hashCode Reference
hashCode 를 재정의 하지 않았을 때 발생할 수 있는 문제
HashMap
에서 키를 클래스로 사용하게 될때 의도와 다르게 동작할 수 있다.올바른 hashCode 재정의 방법
equals
에 포함된 필드는 필수적으로hashCode
에도 포함돼야 한다.equals
에 포함되지 않은 필드는hashCode
에도 포함해서는 안된다.int
형 변수(result
)를 하나 선언하고 핵심 필드(equals
에 포함된)의 hashCode 값으로 초기화<TypeClass>.hashCode()
사용equals
와 동일하게 맞추는것이 중요. 혹은 구현된hashCode
사용. null 일경우 0사용Arrays.hasCode()
사용. 배열이 빈경우 0사용result
에 연산 수행result = 31 * result + c
구현이 너무 복합하지만.. 빠르고 해싱 품질도 나쁘지 않는 방법
해싱 품질을 더욱 향상 시키고 싶다면 Guava Hashing 참조
성능 최적화를 위해
Lazy-Initialization
도 활용 가능Lazy-Initialization
은 https://github.com/NMP-Study/EffectiveJava2022/issues/3 에서 설명 했던 것과 동일하게Thread-Safe
에 대한 고민 필요// 멤버 변수 ...
@Override public int hashCode() { int result = hashCode; long temp;
} }
간단하게 hashCode 재정의 하기
Java Objects.hash
IntelliJ 단축키 지원
Guava Objects.hashCode
IntelliJ 단축키 지원
common-lang3 HashCodeBuilder
IntelliJ 단축키 지원
Lombok @EqualsAndHashCode