hashCode 일반 규약
•
equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 항상 같은 값을 반환해야 한다.
→ 애플리케이션을 다시 실행한다면 이 값이 달라져도 상관없다.
•
equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
•
equals가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다.
→ 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블이 성능이 좋아진다.
hashCode 재정의시 발생가능한 문제들
•
hashCode를 잘못 재정의할 경우, hashCode 일반 규약의 2번째 조항에 관한 문제가 발생할 수 있다.
◦
equals 메서드는 물리적으로 다른 두 객체를 논리적으로 같다고 판단할 수 있다. 하지만 Object에 정의된 기본 hashCode 메서드는 물리적 동치성을 판단하기 때문에 규약의 2번째 조항과 달리 논리적으로 같은 객체를 다르다고 판단하여 서로 다른 hashCode값을 반환한다.
public static void main(String[] args) {
Map<Job, String> m = new HashMap<>();
m.put(new Job("Developer", 1000, 8, 20), "JavaDeveloper");
System.out.println("JavaDeveloper in HashMap = " + m.get(new Job("Developer", 1000, 8, 20)));
}
결과
JavaDeveloper in HashMap = null
Java
복사
•
위의 경우, Job 클래스는 hashCode를 재정의하지 않았기 때문에 논리적으로 동치인 2개의 Job 인스턴스가 서로 다른 hashCode를 반환하여 HashMap에서 null이 반환된다.
좋은 hashCode 메서드를 작성하는 요령
1.
int 변수 result를 선언하고 값 c로 초기화환다.
→ 이때의 c는 해당 객체의 첫 번째 핵심 필드(equals 비교에 사용되는 필드)를 다음의 2.a 방식으로 계산한 hashCode이다.
2.
해당 객체의 나머지 핵심 필드(f) 각각에 대해 다음 작업을 수행한다.
a.
해당 필드의 c를 계산한다.
i.
기본 타입 필드의 경우
•
Type.hashCode(f)를 수행한다.
→ Type은 해당 기본 타입의 박싱 클래스를 의미.
ii.
참조 타입 필드의 경우
•
해당 클래스의 equals메서드가 이 필드의 equals를 재귀적으로 호출해 비교한다면, 이 필드의 hashCode를 재귀적으로 호출한다.
•
계산이 복잡해진다면, 이 필드의 표준형을 만들어서 그 표준형의 hashCode를 호출한다.
•
필드의 값이 null이면 일반적으로 0을 사용한다.
iii.
배열 필드인 경우
•
핵심 원소 각각을 별도의 필드처럼 다룬다.
•
배열에 핵심 원소가 없다면, 상수를(일반적으로 0)을 사용한다.
•
배열의 모든 원소가 핵심 원소라면 Arrays.hashCode를 사용한다.
b.
단계 2.a에서 계산한 hashCode c로 result를 다음과 같은 방식으로 갱신한다.
→ result = 31 * result + c
3.
result 반환
•
위의 방식을 이용하여 Job 클래스의 hashCode 메서드를 재정의하면 다음과 같다.
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + Integer.hashCode(salary);
result = 31 * result + Integer.hashCode(workingHours);
result = 31 * result + Integer.hashCode(vacationDays);
return result;
}
Java
복사
// 두 객체가 논리적으로 동치인 경우 - hashCode가 같다.
public static void main(String[] args) {
Job developer1 = new Job("Developer", 1000, 8, 20);
Job developer2 = new Job("Developer", 1000, 8, 20);
System.out.println("hashCode of developer1 = " + developer1.hashCode());
System.out.println("hashCode of developer2 = " + developer2.hashCode());
}
결과
hashCode of developer1 = 1778879242
hashCode of developer2 = 1778879242
Java
복사
// 두 객체가 논리적으로 동치가 아닌 경우 - hashCode가 다르다.
public static void main(String[] args) {
Job developer1 = new Job("JavaDeveloper", 1000, 8, 20);
Job developer2 = new Job("CppDeveloper", 1200, 8, 20);
System.out.println("hashCode of developer1 = " + developer1.hashCode());
System.out.println("hashCode of developer2 = " + developer2.hashCode());
}
결과
hashCode of developer1 = 641029708
hashCode of developer2 = 416437973
Java
복사
•
주의사항
◦
파생 필드는 해시코드 계산에서 제외해도 무방하다.
◦
equals 비교에 사용되지 않은 필드는 반드시 제외해야 한다.
◦
2.b의 31 * result는 필드를 곱하는 순서에 따라 result값이 달라지게 한다.
→ 클래스에 비슷한 필드가 다수일 때, 해시 효과를 높여준다.
→ ex. String의 hashCode를 곱셉 없이 구현한다면, 모든 아나그램의 hashCode가 같아진다. 즉, 서로 다른 String을 같다고 인식할 수 있다.
◦
31을 곱하는 이유는 홀수이면서 소수(prime number)이기 때문이다.
→ 짝수를 곱하고 overflow가 발생한다면 정보를 잃을 수 있다. 왜냐하면, 2를 곱하는 것은 시프트 연산과 같은 결과를 도출하기 때문이다.
◦
성능을 높이기 위해서 핵심 필드를 생략해서 hashCode를 계산해서는 안 된다.
→ hash 품질이 나빠져서 hash table의 성능을 저하할 수 있다.
hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말자
•
자세한 규칙을 공표하지 않는다면, 클라이언트들이 해당 hashCode값에 의지하지 않게 되고, 해시 기능에서 결함을 발견했거나 계산 방식을 바꿀 여지가 있을 경우 다음 release에서 수정 할 수 있다.
질문
•
equals가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다? → 서로 다른 두 객체의 hashCode가 동일할 수 있는가?
•
필드의 표준형이란?