-
Item 11. equals를 재정의하려거든 hashCode도 재정의하라백수의 개발/이펙티브 자바 2019. 8. 20. 17:43
equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다.
이는 HashMap이나 HashSet같은 컬렉션의 원소로 사용할 때 문제를 일으킬 수 있기 때문이다.
hashCode의 규약
- equals에 비교되는 정보가 변경되지 않았다면, hashCode 메서드는 일관되게 항상 같은 값을 반환해야 한다.(단, 애플리케이션 재실행 시 값이 달라져도 상관없다.)
- equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode도 동일해야 한다.
- equals가 두 객체를 다르다고 판단하더라도, 두 객체의 hashCode가 다른 값을 반환할 필요는 없다.(단, 다른 값을 반환해주어야 해시테이블의 성능이 향상된다.)
hashCode 재정의가 안되거나, 잘 못된 경우
대부분 hashCode 재정의 시 hashCode의 두번째 규약을 어기기 쉽다. 즉, 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.
만약 hashCode를 재정의 하지 않을 때 어떤 문제가 발생하는지 아래 코드를 통해 살펴보자.
Map<PhoneNumber, String> m = new HashMap<>(); m.put(new PhoneNumber(707, 867, 5309), "제니"); m.get(new PhoneNumber(707, 867, 5309)); // "제니" (X), null (O)
위의 코드를 보면 get(PhoneNumber)을 통해 "제니"가 나올 것으로 기대했지만, 실제로는 hashCode가 재정의되지 않아 원하는 데이터를 찾지 못하고 null을 반환한다. 이는 hashCode를 재정의 해주면 된다.
public class PhoneNumber{ ... @Override public int hashCode(){ return 42; } ... ]
위와 같이 hashCode를 재정의하면 "제니"를 가져올 수 있다. 하지만 hashCode가 동일하여 모든 객체에 대해 동일한 해시값을 반환한다.
이때문에 해시테이블의 버킷 하나에 담겨 마치 Linked List처럼 동작하게 되고, 그 결과 평균 수행 시간이 O(1)인 해시테이블이 O(n)으로 느려져, 객체가 많아지면 도저히 쓸 수가 없게 된다.
hashCode 작성하는 법
아래 코드는 기본적으로 hashCode를 작성하는 방법이다.
class HashCode { int firstValue; long secondValue; String thirdValue; String[] forthValue; @Override public int hashCode() { // 1. int변수 result를 선언한 후 첫번째 핵심 필드에 대한 hashcode로 초기화 한다. // 초기화 방법은 아래와 같이 데이터 타입에 맞게 해주면 된다. int result = Integer.hashCode(firstValue); // 31 * result + hashCode(C)으로 다음 result를 계산한다. // 2. 기본타입 필드라면 Type.hashCode(f)를 실행한다 // Type은 기본타입의 Boxing 클래스이다. result = 31 * result + Long.hashCode(secondValue); // 3. 참조타입이라면 참조타입에 대한 hashcode 함수를 호출 한다. // 값이 null이면 0을 더해 준다. result = 31 * result + (thirdValue == null ? 0 : thirdValue.hashCode()); // 4.1 필드가 배열이라면 핵심 원소를 각각 필드처럼 다룬다. for (String elem : forthValue) { result = 31 * result + (elem == null ? 0 : elem.hashCode()); } // 4.2 배열의 모든 원소가 핵심필드이면 Arrays.hashCode를 이용한다. result = 31 * result + Arrays.hashCode(forthValue); //result를 리턴한다. return result; } }
만약, 해시코드를 계산하는 비용이 크다면, 매번 새로 계산하기 보다는 캐싱을 고려해야할 것 이다.
해당 타입의 객체가 주로 해시의 키로 사용 될 것 같다면 아래와 같이 인스턴스가 만들어질 때 해시코드를 계산해두면 효율적이다.
public class HashCode{ private int hashCode; ... public HashCode(...){ hashCode = 31 * result + c; } ... @Override public int hashCode(){ return hashCode; } }
해시의 키로 사용되지 않는 경우라면 아래와 같이 hashCode가 처음 불릴 때 계산하는 지연 초기화도 가능하다.
public class HashCode{ private int hashCode; ... @Override public int hashCode(){ int result = hashCode; if(result == 0){ result = 31 * result + c; hashCode = result; } return result; } }
만약, 위와 같이 지연 초기화를 하게 된다면 스레드 안정성도 고려해주어야한다.
성능을 높이기 위해 해시코드를 계산할 때 핵심 필드를 생략해서는 안된다. 이는 해시 품질이 나빠져 해시테이블의 성능을 심각하게 떨어뜨릴 수도 있다.
마무리
equals를 재정의할 때는 hashCode도 반드시 재정의해야 한다. 그렇지 않으면 프로그램이 제대로 동작하지 않을 수 있다.
hashCode는 Object의 API문서에 기술 된 일반 규약을 따라야 하며, 서로 다은 인스턴스라면 되도록 해시코드도 서로 다르게 구현하는 것이 좋다.
* AutoValue 프레임워크를 사용하면 equals와 hashCode를 자동으로 만들어준다.
'백수의 개발 > 이펙티브 자바' 카테고리의 다른 글
Item 13. clone 재정의는 주의해서 진행하라 (0) 2019.08.27 Item 12. toString을 항상 재정의하라 (0) 2019.08.22 Item 10. equals는 일반 규약을 지켜 재정의하라 (0) 2019.08.13 Item 9. try-finally보다는 try-with-resource를 사용하라 (0) 2019.08.09 Item 8. finalizer와 cleaner 사용을 피하라 (0) 2019.08.07