equals메소드를 오버라이드하게되면 반드시 hashcode도 오버라이딩 해줘야한다.
이는 규칙이며, 하지 않게되면 HashMap, HashSet 등 Hash기반의 컬렉션들과 개발한 클래스들을 사용할 때 제대로 작동하지 않을 것이다.
Java API 내에서도 명시되어 있다.
The general contract of hashCode is:
- Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
- If two objects are equal according to the equals method, then calling the hashCode method on each of the two objects must produce the same integer result.
- It is not required that if two objects are unequal according to the equals method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
오버라이딩에 실패했을 때 위배되는 주요 조항은 두 번째 조항이다. 두 객체가 있을 때, equals를 통해서 두 객체가 논리적으로 동일한 지 알 수 있다 hashCode는 두 객체가 논리적으로 동일한지 알 수 없다.
import java.util.HashMap;
import java.util.Map;
public class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(short areaCode, short prefix, short lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNumber = lineNumber;
}
private void rangeCheck(short arg, int max, String name) {
if (arg < 0 || arg > max) {
throw new IllegalArgumentException(name + ": " + arg);
}
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof PhoneNumber)) {
return false;
}
PhoneNumber pn = (PhoneNumber) obj;
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
@Override
public int hashCode() {
return super.hashCode();
}
public static void main(String[] args) {
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber((short) 707, (short) 867, (short) 5309), "jeong");
System.out.println(m.get(new PhoneNumber((short) 707, (short) 867, (short) 5309)));
}
}
이와 같은 코드에서 결과값은 true일 거 같지만 null을 리턴한다.
왜냐면 new를 이용해서 새로운 인스턴스를 만들기 때문에 hashCode에서 메소드를 오버라이드하지않아서 두 인스턴스가 서로 다른 해시 코드 값을 갖게된 것이다.
이러한 부분은 hashcode 메소드를 오버라이딩해서 원하는 값을 출력할 수 있다.
이해한 바로는 위의 코드의 경우 HashMap, HashSet과 같은 자료구조에서 의미상으로 들어간 데이터(new PhoneNumber((short) 707, (short) 867, (short) 5309))이 동일한 value를 갖고 있는데, 이 값의 인스턴스가 달라 값을 가져오지 못하는 문제로 인해 잘못된 상황이다를 얘기하는 것 같고 이 문제는 hashcode 메서드의 오버라이딩을 통해 해결 가능하다 같다.
0이 아닌 상수값을 result 에 정리한다. 이 때 상수값은 소수가 좋다.
객체의 equals에서 비교하는 주요 필드 변수 f에 대해 다음과 같이 수행한다.
필드에 대한 int 타입의 해시 코드 c를 다음과 같이 산출한다.
위의 단계에서 구현한 해시코드 c를 result에 합산한다.
result를 반환한다.
hashcode의 메소드 작성이 끝나면 동일한 인스턴스들이 같은 값을 갖는지 검토하고 잘못된 점이 있으면 수정하여 완료한다.
해시코드 산출시 파생 필드는 제외시킬 수 있는데, 이 말은 해시코드 연산에 포함되는 필드로부터 값이 산출될 수 있는 어떤 필드도 무시할 수 있다고 한다. equals 메소드에서 비교하지 않는 필드는 제외시켜야한다. 그렇지 않으면 2번째 조항을 어길 수 있기 때문이다.
따라서 아래와 같이 바꿀 수 있다.
@Override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
이렇게 바꾸면 true가 나오게 된다.
불변이면서 해시 코드 연산이 중요한 클래스라면 해시 값을 매번 연산하는 것이 아니라, 객체 내부에 해시 코드를 저장하는 것을 고려해 볼 수 있다. 이런 경우엔 인스턴스가 생성될 때 해시 코드를 산출해야하고, 그게 아니라면, 늦초기화를 통해 할 수도 있다.
해시 코드를 산출할 때 성능 향상을 이유로 객체의 중요 부분을 제외시키지 말자. 품질 저하로 인해 해시 테이블의 성능을 떨어뜨릴 수 있다.
Effective Java ed2