- 이 글은 Effective Java 를 기반으로 작성되었습니다.
- hashCode 재정의의 중요성
equals 를 재정의할 때는 반드시 hashCode 도 재정의 해야 한다. 그러지 않으면 HashSet, HashMap, HashTable 과 같은 Hash 컬렉션을 사용할 때 문제가 발생할 수 있다.
Object 클래스 명세에서 hashCode 에 관한 일반 규약을 살펴보자.
- equals 가 사용하는 정보가 변경되지 않는다면 hashCode 를 여러 번 호출해도 동일 hash 값을 반환해야 한다.
- equals 메소드가 같다고 판정한 두 객체는 같은 hash 값을 반환해야 한다.
- equals 메소드가 다르다고 판정한 두 객체의 hash 값이 같을 수는 있다. 하지만 이런 일이 자주일어나면 성능이 떨어진다는점은 알고 있어야 한다.
- 규약의 위반
위의 3 가지 일반 규약 중 hashCode 를 재정의 하지 않았을 때, 문제가 되는 핵심규약은 2 번째 규약이다. 어떤 클래스의 속성 값이 모두 같은 2 객체가 있다고 가정해보자. 이 속성들이 같을 때 논리적으로 같다고 판정하고 싶다면 equals 메소드를 재정의할 것이다. 하지만 hashCode 를 재정의하지 않으면 객체의 hash 값은 다른값을 반환한다.
public class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
super();
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 9999, "line number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
private static void rangeCheck(int arg, int max, String name) {
if(arg < 0 || arg > max) {
throw new IllegalArgumentException(name + ": " + arg);
}
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof PhoneNumber))
return false;
PhoneNumber other = (PhoneNumber) obj;
return areaCode == other.areaCode &&
lineNumber == other.lineNumber &&
prefix == other.prefix;
}
}
위와 같이 PhoneNumber 클래스가 있다. areaCode, lineNumber, prefix 가 같을 시 동등하다 라고 정의하기 위해 equals 메소드를 재정의하고 hashCode 는 정의하지 않았다.
public class HashTest {
public static void main(String[] args) {
PhoneNumber phoneNumber1 = new PhoneNumber(123, 123, 1234);
PhoneNumber phoneNumber2 = new PhoneNumber(123, 123, 1234);
System.out.println("phoneNumber1.equals(phoneNumber2): " + phoneNumber1.equals(phoneNumber2));
System.out.println("phoneNumber1 hash: " + phoneNumber1.hashCode());
System.out.println("phoneNumber2 hash: " + phoneNumber2.hashCode());
}
}
.....
phoneNumber1.equals(phoneNumber2): true
phoneNumber1 hash: 705927765
phoneNumber2 hash: 366712642
phoneNumber1 과 phoneNumber2 는 같지만 각 객체의 hash 값을 출력해보면 다른것을 알 수 있다. 이게 문제가 될까? equals 만 같다고 판단하면 문제가 없는것 아닌가? 문제는 Hash 기반의 Collection 을 사용할 때 나타난다.
Map<PhoneNumber, String> phoneBook = new HashMap<>();
phoneBook.put(phoneNumber1, "A");
System.out.println("phoneBook.containsKey(phoneNumber2): " + phoneBook.containsKey(phoneNumber2));
......
phoneBook.containsKey(phoneNumber2): false
HashMap 에 put 을 할 때에는 phoneNumber1을 key 로, 포함되어있는지 검사할때에는 phoneNumber2 로 검사를 하였다. 다른 객체이긴 하지만 equals 메소드로 객체의 속성값이 모두 같으면 동등하다고 정의했으므로 key 가 포함되어있다고 나와야 한다. 하지만 결과는 전혀 다르다.
- hashCode 정의
hashCode 를 정의해보자. hashCode 는 int 형을 반환하는 메소드 시그니처를 갖는다. 아래와 같이 그냥 단일값으로 반환하면 될까?
@Override
public int hashCode() {
return 1;
}
hashCode 가 반환하는 값은 equals 에 정의한 Field 들을 기준으로 균등한 값을 가질수록 성능이 좋아진다. 위와 같은 메소드의 경우 모든 객체가 같은 값을 가지게 되어 O(1) 의 시간을 기대하고 쓰는 Hash 라는 자료구조의 이점을 전혀 살리지 못하고 O(n) 이 되어버린다.
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + areaCode;
result = prime * result + lineNumber;
result = prime * result + prefix;
return result;
}
Hash 값을 어떻게 생성해야 균등한지 분포도가 좋은지는 알기 어려우며 거의 수학적 영역에 가깝다. 다행스럽게도 보통 IDE 에서 hashCode 메소드를 자동으로 생성해준다.
- 그 외 고려사항들
hashCode 함수 재정의시 추가적으로 고려해야할 몇 가지 사항들을 알아보자.
첫째로 다른값에서 파생될 수 있는 값은 무시할 수 있다. hashCode 는 해당 객체의 동등성에 대해 최대한 유일값을 반환해야 한다. 다른값에서 파생된다는 건 중복 정보 이므로 이를 고려할 필요 없다.
둘째로 equals 에 쓰이지 않는 Field 는 반드시 제거해야 한다. 객체의 동등성을 판별하는데 a, b 속성만 사용되는데 hash 값을 생성할때에 c 속성까지 넣어버리면 a와 b가 같아 동등한 객체여도 c값이 다르면 다른 hash 를 반환한다. 이는 단순한 성능저하를 넘어서 hashCode 의 규약을 깨는것이기 떄문에 반드시 지켜줘야 한다.
셋째로 성능 개선을 위해 hashCode 값 계산에서 주요 속성을 제거할때에는 신중해야 한다. 만약 제거하기로 한 해당속성값만 다른 객체가 아주 많이 생기면 성능상 불이익을 받을 수 있기 때문이다.
'Language > Java' 카테고리의 다른 글
이펙티브 자바 - clone 재정의 (0) | 2021.10.11 |
---|---|
이펙티브 자바 - toString 재정의 (0) | 2021.10.04 |
이펙티브 자바 - equals 재정의 (0) | 2021.09.23 |
이펙티브 자바 - 종료자 사용을 피하라 (0) | 2021.09.22 |
이펙티브 자바 - 유효기간이 지난 객체 참조 (0) | 2021.09.19 |
댓글