-
이펙티브 자바 Item 11 - equals를 재정의하려거든 hashCode도 재정의하라언어/Effective Java 2024. 5. 28. 16:12
equals를 재정의한 클래스는 hashCode도 재정의 해야한다.
그렇지 않으면 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제가 발생한다.
동등성: 객체가 주소값이 다르더라도 내용이 같다면 같은 거로 보는 것 (equals())
동일성: 객체의 주소값이 다르면 아무리 같은 내용이더라도 같지 않다고 보는 것 (ex == )
값만 같은지 아예 같은 객체인지 비교하기 위해선 동등성과 동일성이 보장되어야함!!
1.hashCode 일반 규약
- equals 비교에 사용되는 정보가 변경되지 않았다면, hashCode도 변하면 안 된다.
- 애플리케이션을 다시 실행한다면, 이 값이 달라져도 상관없음
- equals가 두 객체가 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환한다.
- 논리적으로 같은 객체는 같은 해시코드를 반환해야한다.
- equals가 두 객체를 다르다고 판단했더라도, hashCode는 꼭 다를 필요는 없다.
- 하지만 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
2.논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.
equals가 물리적으로 다른 두 객체를 논리적으로 같다고 할 때, hashCode는 서로 다른 값을 반환한다.
Map<PhoneNumber, String> map = new HashMap<>(); map.put(new PhoneNumber(010,1234,5678), new Person("리치")); //위코드에 map.get(new PhoneNumber(010,1234,5678))를 실행하면 "리치"가 아닌 null을 반환
- PhoneNumber 클래스는 hashCode를 재정의하지 않았기 때문에 논리적 동치인 두 객체가 서로 다른 해시코드를 반환하여 get 메서드는 엉뚱한 해시 버킷에 가서 객체를 찾으려하는 것이다.
- HashMap은 해시코드가 서로 다른 엔트리끼리는 동치성 비교를 시도조차 않도록 최적화됨
- 모든 동치에 같은 hashCode를 반환하는 코드는 안된다.
- 모든 객체가 해시테이블의 버킷 하나에 담겨 마치 연결리스트처럼 작동하는데, 이런식이면 O(n)이 되어 느려서 못 쓴다.
@Override public int hashCode() { return 42; } //동치 해시코드
- map에서 get()관련 equals와 hashcode 사용
final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // 찾은 hash값이 맞는지 비교하고 맞다면 equals를 통해 값을 준다. ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { // 같은 해쉬값이고 equals의 결과가 다른경우 다음노드를 조회 if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
- HashMap은 들어온 키 객체를 hash값으로 바꾸고 이걸 주소값으로 쓰는 값을 찾아
- hash값 키값 equals를 비교하여 다 통과하면 해당 노드를 반환한다.
3. 좋은 해시 함수라면 서로 다른 인스턴스에 다른 해시코드를 반환한다.
1. int 변수인 result를 선언한 후 값을 초기화한다. (값 c라고 가정)
- 이때 c는 해당 객체의 첫번째 핵심필드를 2.1 방식으로 계산한 값이다.
- 여기서 핵심 필드는 equals 비교에 사용되는 필드이다.
2. 해당 객체의 나머지 핵심 필드인 f 각각에 다음 작업을 수행한다.
- 해당 필드의 해시코드 c를 계산한다.
- 기본 필드라면 Type.hashCode(f)를 수행
- 참조 필드면 equals메소드가 이 필드의 equals를 호출 (필드의 hashCode 반환)
- 필드가 배열이라면, 핵심 원소 각각을 별도 필드 처럼 다룬다.
- 모든 필드가 핵심 원소라면 Arrays.hashCode를 사용한다.
- 2.1 단계에서 계산한 해시코드 c로 result를 갱신한다.
- result = 31*result+c;
주의할 점
- equals비교에 사용되는 필드에 대해서만 해시코드를 계산한다.
- 성능 높인답시고 해시코드 계산시 핵심 필드를 생략해서는 안된다.
- 만약 hash로 바꾸려는 필드가 기본 타입이 아니면 해당 필드의 hashCode를 불러 구현한다.
- 참조 필드가 null일 경우 0을 사용
- 31을 곱하는 이유는 비슷한 필드가 여러개일 때 해시효과를 높여주기 위해서다.
- 비슷한 값을 더하다보면 같은 값이 나올 확률이 높음
@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((accountNo == null) ? 0 : accountNo.hashCode()); result = prime * result + balance; result = prime * result + ((ownerName == null) ? 0 : ownerName.hashCode()); return result; }
Objects 클래스의 hashCode 메서드는 성능이 살짝 아쉽다
-> 입력 인수를 담기 위한 배열이 만들어지고, 입력 중 기본 타입이 있다면 박싱과 언박싱을 거침
4. hashCode의 캐싱과 지연 초기화
- 클래스가 불변이고 해시코드 계산 비용이 크다면, 매번 새로 계산하기 보다 캐싱을 고려해야 한다.
- 이 타입의 객체가 주로 해시의 키로 사용될 것 같다면 인스턴스가 만들어질 때 해시코드로 계산해둔다.
- 해시의 키로 사용되지 않는 경우라면 hashCode가 처음 불릴 때 계산하는 지연 초기화하면 좋다.
- 필드를 지연 초기화 하려면 그 클래스가 thread-safe가 되도록 동기화에 신경 쓰는 것이 좋다.
//지연 초기화에 대한 스레드 안전성 고려 - 한번 계산하고 돌려쓰기 private int hashCode; @Override public int hashCode() { int result = hashCode; // 초기값 0을 가진다. if(result == 0) { int result = Integer.hashCode(areaCode); result = 31 * result + Integer.hashCode(areaCode); result = 31 * result + Integer.hashCode(areaCode); hashCode = result; } return result; }
'언어 > Effective Java' 카테고리의 다른 글
이펙티브 자바 Item 12 - toString은 항상 재정의하라 (0) 2024.05.29 이펙티브 자바 Itme 10 - equals는 일반 규약을 지켜 재정의 하라 (0) 2024.05.28 이펙티브 자바 Item 9 - try-finally 대신 try-with-resources를 사용하라 (0) 2024.05.28 이펙티브 자바 Item7 - 다 쓴 객체의 참조를 해제하라 (0) 2024.05.28 이펙티브 자바 Item6 - 불필요한 객체 생성을 피하라 (0) 2024.05.27 - equals 비교에 사용되는 정보가 변경되지 않았다면, hashCode도 변하면 안 된다.