본문 바로가기
프로그래밍 놀이터/디자인 패턴, 리펙토링

[Design Pattern/Java] equals 메소드를 오버라이드 할 때는 hashCode 메소드도 항상 같이 오버라이드 하자.

by 돼지왕왕돼지 2012. 2. 22.
반응형
hashCode 를 왜 override 해줘야 하는데?

 hashCode 메소드를 제대로 오버라이드 하지 않아 코드 결함이 생기는 경우가 흔합니다. equals 메소드를 오버라이드 하는 모든 클래스에서는 반드시 hashCode 메소드도 오버라이드 해야 합니다. 그렇지 않으면, Object.hashCode 메소드의 보편적 계약을 위반하게 되므로, HashMap 과 HashSet 및 HashTable 을 포함하는 모든 해시( hash ) 기반의 컬렉션들과 우리 클래스를 같이 사용할 때 우리 클래스가 올바르게 동작하지 않을 가능성이 매우 높습니다.


 

hashCode 의 보편적 계약사항이 뭐길래?
 
1. 애플리케이션 실행 중에 같은 객체에 대해 한 번 이상 호출되더라도 hashCode 메소드는 같은 정수를 일관성 있게 반환해야 한다. ( 객체 값이 변환되지 않는다면.. ) 단, 이 정수 값은 애플리케이션이 매번 다시 실행될 때마다 일관되게 같을 필요는 없다.
 
2. equals(Object) 메소드 호출 결과 두 객체가 동일하다면, 두 객체 각각에 대해 hashCode 메소드를 호출했을 때 같은 정수 값이 나와야 한다.
 
3. equals(Object)메소드 호출 결과 두 객체가 다르다고 해서 두 객체 각각에 대해 hashCode 메소드를 호출했을 대 반드시 다른 정수 값이 나올 필요는 없다. 그러나 같지 않은 객체들에 대해 hashCode 메소드에서 서로 다른 정수 값을 반환하면, 이 메소드를 사용하는 해시 컬렉션들( HashMap, HashSet, HashTable 등 ) 의 성능을 향상시킬 수 있음을 알아야 한다.
 
 
 
 
 
위의 보편적 계약사항 중에 뭘 위반한다는 거야?
 
- hashCode 메소드의 오버라이딩에 실패했을 때, 혹은 오버라이딩 하지 않았을 때 위배되는 주요 조항은 두번째 것으로써, 동일한 객체들은 같은 해시 코드 값을 가져야 한다는 것. Object 의 hashCode 메소드에서는 비교되는 두개의 객체가 논리적으로 같다는 것을 알 수 없기 때문에 hashCode 호출에 대해 임의의 수 두 개를 반환한다.
 
 
 


그럼 hashCode 메소드를 어떻게 오버라이딩 해야해? ( hashCode 값을 어떻게 산출해?  )

- 먼저 주의할 것은 상수 값을 return 해서는 안된다는 거야. 최악의 시나리오인데, 모든 해당 객체에 대해 같은 값을 내뱉기 때문이지.
 
- 좋은 해시 메소드는 동일하지 않은 객체들에 대해 서로 다른 해시 코드를 만드는거야. 이상적인 경우는 동일하지 않은 인스턴스들에 대해 해시 메소드가 모든 가능한 해시 값을 고르게 분산시켜주어야 해. 이것을 완벽하게 구현하는 것은 어렵지만, "거의 균일하게" 분포하는 것은 다음의 방법을 따르면 돼지.

 

1. 0이 아닌 어떤 상수 값을 result 라는 int 변수에 저장. ex ) int result = 17;
 
2. hashCode 를 override 하는 객체의 주요 필드 f 에 대해 다음을 수행. ( 주요 필드란 equals() 에서 비교하는 필드들. )
a. 각 필드에 대한 int 타입의 해시 코드 c 를 다음과 같이 산출.
i. boolean 은 f ? 1 : 0
ii. byte, char, short, int 면 (int) f
iii. long 이면 (int) ( f ^ ( f >>> 32 ) )
iv. float 이면 Float.floatToIntBits( f )
v. double 이면 Double.doubleToLongBits( f ) 후 iii 적용.
vi. 객체 참조일 경우, 참조되는 object 의 hashCode. null 일 경우는 0
vii. 배열이라면 배열의 각 요소를 별개의 필드로 처리.
 
b. a의 각 단계에서 구한 해시 코드 c를 result 에 합계한다. ex ) result = 31 * result + c. ( 이 공식을 각 필드마다 수행 )
- 여기서 31 은 소수이기 때문에 선택. 다른 값을 해도 되지만, 소수가 무난 & 관례.
- 연산하는 순서에 따라 hashCode 값이 달라지는 것에 주의. ( 어떤 필드를 먼저 result 코드에 반영시키느냐의 순서. )
 
3. result 값 반환
 
4. hashCode 메소드 작성이 끝나면 동일한 인스턴스들이 같은 값의 해시 코드를 갖는지 검토.

 

 
주의. equals 메소드에서 비교하지 않은 필드는 제외.
 
참고. 불변이면서 해시 코드 연산 비용이 중요한 클래스라면, 객체 내부에 해시 코드를 저장하는 것을 고려. initialize 순간에 저장할 수도 있고, lazy initialization 할 수도 있다.
 
 
ex code)

 

@Override
public int hashCode(){
   int result = 17;
   result = 31 * result + phoneNumber;
   result = 31 * result + isCellPhone? 1 : 0;
   result = 31 * result + name.hashCode();
   return result;
}

 





또 다른 주의사항은 없어?? 뭐 성능이 떨어지거나 그러진 않나??

- 해시 코드를 산출할 때 성능 향상을 이유로 객체의 중요 부분을 제외시키면 안돼. 그로 인해서 해시 메소드의 실행 속도는 더 빨라질 수 있지만, 너무 느려 사용 불가능한 정도까지 해시 테이블들의 성능을 떨어뜨릴 수 있어. 해시 테이블들의 성능이 떨어지는 이유는.. 일반적으로 Hash 관련 class 들이 버킷을 LinkedList 형태로 사용하는데, 한 버킷에 자료가 많이 들어가게 되면 (LinkedList 에 담겨진 자료가 많을수록) 탐색에 시간이 많이 걸리기 때문이지.

 

 
 

 

반응형

댓글4

  • Jino 2015.04.27 22:27

    좋은 글 감사합니다. 감사히 잘 사용하겠습니다.
    답글

  • 지나가다 2021.08.03 03:20

    마지막에 아래 문장의 뜻이 뭔가요?

    "다른 객체에 대해 같은 객체의 처리를 하기 때문에 같은 버킷에 위치하게 되고, Hash 관련 class 들은 링크드 리스트 ( Linked list ) 를 다시 생성하기 때문이지."
    답글

    • https://javascriptonthego.wordpress.com/2017/05/03/what-is-a-hash-table/
      일단 HashTable 에 대해서는 위 링크를 한번 참고해보시면 좋을 것 같습니다.

      HashTable 은 Key - LinkedList 형태의 매핑관계를 가지고 있는데, Collision 으로 특정 Bucket 에 자료가 몰리게 되면 LinkedList 에 자료가 많이 들어가게 됩니다.
      LinkedList 의 특성상 자료가 많을수록 성능이 떨어지게 되겠죠?

      "다시 생성" 관련해서는 저도 시간이 너무 지나서 정확히 어떤 내용인지 기억이 안 나는데요... ㅠ
      핵심은 "LinkedList 에 자료가 몰리는 현상이 발생하면 성능이 떨어진다" 입니다.