개발하는 두더지

[Effective Java 규칙9] equals를 재정의할 때는 반드시 hashCode도 재정의하라 본문

Java,Android

[Effective Java 규칙9] equals를 재정의할 때는 반드시 hashCode도 재정의하라

덜지 2018. 9. 14. 12:04

[Effective Java 규칙9] equals를 재정의할 때는 반드시 hashCode도 재정의하라

Effective Java 2/E 책과 구글링을 통해 내용을 정리하고 개인적인 견해가 포함된 글입니다.


많은 버그가 hashCode 메서드를 재정의하지 않아서 발생한다.  그로므로 equals 메서드를 재정의할 때 hashCode 메서드도 반드시 재정의 해야한다.

Object 클래스 명세에 정의된 내용을 보면

- 응용프로그램 실행 중에 같은 hashCode를 여러번 호출하면 항상 같은 값을 반환해야한다. 다만 프로그램이 종료되었다가 다시 실행될 때 같은 값이 나올 필요는 없다.

- equals 메서드가 같다고 판정한 두 객체의 hashCode 값은 반드시 같아야 한다.

- equals 메서드가 다르다고 판정한 두 객체의 hashCode 값은 꼭 다를 필요는 없다. 다만 서로 다른 hashCode 값이 나오면 해시 테이블의 성능이 향상될 수 있다는 점은 이해하고 있어야 한다.


equals 메서드를 재정의하고 hashCode 메서드를 재정의하지 않으면 두번째 규약을 위반하게 된다.

예를들어 아래 클래스를 보자

public class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;

public PhoneNumber(int areaCode, int prefix, int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(areaCode, 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 o) {
if(!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.areaCode == areaCode
&& pn.prefix == prefix
&& pn.lineNumber == lineNumber;
}
}


Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 666, 7899), "KIM");
System.out.println(m.get(new PhoneNumber(707, 666, 7899)));

결과는 KIM이 나오는 것이 아닌 null이 반환된다. hashCode 메서드를 재정의 하지 않았기때문에 다른 해시코드 값을 갖게된다. 따라서 get 메서드는 put 메서드가 객체를 저장한 것과 다른 해시 버킷에서 값을 검색하게 된다. 운이 좋으면 같은 해시 버킷에서 검색할 수 있겠지만 대부분 null을 반환할 것이다. 왜냐하면 HashMap은 성능 최적화를 위해 내부에 보관된 항목의 해시 코드를 캐시해두고, 캐시된 해시 코드가 없는 객체는 동일성 검사조차 하지 않기때문이다.


PhoneNumber 객체를 생성하여 메모리에 올리고 그 객체를 put, get하면 원하는 값을 가져올 수 있지만 책의 예제에서는 전부 객체를 생성했을 때 같은 해시코드를 가지게하여 테스트하는 방식을 사용한 듯하다.


이상적인 해시 함수에 가까운 함수를 만들어서 사용하는 방법을 이용한다. 해시 충돌을 피하며 서로 다른 객체들을 사용할 수 있는 해시값에 균등하게 분배하는 기능을 만드는 것이다. 책에 나와있는 가이드는 적지 않았지만 예를들면 아래와 같이 코드를 작성할 수 있다.

private volatile int hashCode;
@Override
public int hashCode() {
int result = hashCode;
if(result == 0) {
result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
hashCode = result;
}
return result;
}

PhoneNumber 객체의 중요 필드만 입력으로 사용하여 해시코드를 만들어 냈으므로 동일한 필드를 갖고있는 객체는 같은 해시 코드를 반환하게 된다.

간단하면서도 충분히 빠르고, 다른 전화번호를 다른 해시 버킷에 할당한다.

주의할 것은 성능을 개선하려고 객체의 중요 부분을 해시 코드 계산 과정에서 생략하면 안된다는 것이다. 속도는 빠를지 몰라도 해시 값 품질이 좋지 않기때문에 해시 테이블의 성능을 엄청나게 떨어뜨릴 수 있다.

//TODO 해시 테이블이 어떻게 돌아가는지 찾아보자


책에있는 지침대로하면 꽤 쓸만한 해시함수가 나오지만 가장 뛰어나다고는 할 수 없다. 

//TODO 1.6 기준의 책인데 과연 1.8 1.9, 1.10에서는 뛰어난 해시코드가 포함되었는가? 확인해볼 것


Comments