티스토리 뷰
Item 11. equals를 재정의하려거든 hashcode도 재정의하라
Jordy-torvalds 2019. 12. 8. 18:50equals를 재정의 했을 때 hashcode도 재정의 해야하는 이유
재정의 하지 않을 시 hashcode 일반 규약을 어기게되어 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다. 아래는 object 명세에서 발췌한 규약이다.
- equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashcode 메서드는 몇번을 호출 해도 일관되게 같은 값을 반환해야한다. 단, 애플리케이션 재 실행시에는 값이 변경되어도 상관 없다.
- equals(object)가 두 객체를 같다고 판단했다면, 두 객체의 hashcode는 똑같은 값을 반환해야 한다.
- equals(object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashcode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.
hashcode 재정의 주의사항
hashcode 재정의를 잘못했을 때 크게 문제가 되는 조항은 두 번째다. 즉, 녹리적으로 같은 객체는 같은 해시코드를 반환해야 한다.
equals로 물리적으로 다른 두 객체를 논리적으로 같다고 할 수 있지만, object의 기본 hashcode메서드는 이 둘이 전혀 다르다고 판단하여, 규약과 달리 서로 다른 값을 반환하다.
아래 코드를 보자.
/* [code: Item11_main.java] */
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber((short) 010,(short)1234,(short)5678), "죠르디");
String name = m.get(new PhoneNumber((short) 010,(short)1234,(short)5678));
System.out.println("name = " + name);
/* [console] */
null
위 코드를 실행했을 때 아래와 같이 콘솔에 출력된다. 왜 그런 것일까? 명백히 put 했을 때의 PhoneNumber와 get할 때의 PhoneNumber는 논리적을 동일하다. 하지만, 두 Object의 기본 hashcode를 사용하고 있기 때문에 논리적 동치인 두 객체가 서로 다른 해쉬코드를 반환하여 두 번째 규약을 지키지 못한다.
그 결과 get메서드는 엉뚱한 해시버킷에 가서 객체를 찾으려 한 것이다.
그렇다면 콘솔에 죠르디란 텍스트가 출력되도록 hashcode를 재정의해야 하는데 그 방법을 알아보도록하자.
hashcode 메서드 작성 요령.
- int 변수 result를 선언한 후 값 c로 초기화한다. 이떄 c는 해당 객체의 첫번쨰 핵심필드(equals 비교에 사용되는 필드)를 2.1 방식으로 계산한 해시코드.
- 해당 객체의 나머지 핵심 필드 f 각각에 대해 다음 작업을 수행한다
- 해당 필드의 해시코드 c를 계산한다.
- 기본 타입필드라면 Type.hashCode(f) 를 수행. 여기서 Type은 해당 기본타입의 박싱클래스
- 참조 타입 필드면서 이 클래스의 equals 메서드가 이 필드의 equals를 재귀적으로 호출해 비교한다면, 이 필드의 hashcode를 재귀적으로 호출한다. 계산이 복잡해질 거 같으면 이 필드의 표준형을 만들어 그 표준형의 hashCode를 호출한다. 필드의 값이 null이면 0을 사용한다.
- 필드가 배열이라면, 핵심 원소 각각을 별도 필드처럼 다룬다. 배열에 핵심 원소가 없다면 단순히 상수(0을 추천)을 사용한다. 모든 원소가 핵심 원소라면 Arrays.hashCode를 사용한다.
- 단계 2.1에서 계산한 해시코드 c로 result를 갱신한다.
result = 31 * result + c;
- 해당 필드의 해시코드 c를 계산한다.
- result를 반환한다.
위를 참조하여 아래와 같이 구현하자. hashcode 뿐만 아니라 equals도 빠뜨리지 말아야 한다.
/* [code: PhoneNumber.java] */
public class PhoneNumber {
private short num1, num2, num3;
public PhoneNumber(short num1, short num2, short num3){
this.num1 = num1;
this.num2 = num2;
this.num3 = num3;
}
@Override
public boolean equals(Object o){
if( o == this)
return true;
if(!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber) o;
return pn.num1 == num1 && pn.num2 == num2
&& pn.num3 == num3;
}
@Override
public int hashCode() {
int result = Short.hashCode(num1);
result = 31 * result + Short.hashCode(num2);
result = 31 * result + Short.hashCode(num3);
return result;
}
}
/* [console] */
name = 죠르디
PhoneNumber의 equals와 hashcode를 수정한 이후에는 콘솔에 정상적으로 출력된다.
hashcode 생성 방법은 Objects.hash()를 사용하는 방법과 지연 초기화 하는 방법이 있다.
/* [code: PhoneNumber.java] */
/* [comments: Objects.hash() 사용] */
// ...
@Override
public int hashCode() {
return Objects.hash(num1,num2,num3);
}
//...
/* [code: PhoneNumber.java] */
/* [comments: 지연초기화 사용] */
// ...
private int hashcode; // 자동으로 0으로 초기화된다.
@Override
public int hashCode() {
int result = hashcode;
if (result == 0){
result = Short.hashCode(num1);
result = 31 * result + Short.hashCode(num2);
result = 31 * result + Short.hashCode(num3);
}
return result;
}
//...
해시코드 주의사항
- 성능을 높인답시고 해시코드를 계산할 때 핵심 필드를 생략해서는 안 된다.
- hashCode가 반환하는 값의 새성 규칙을 API 사용자에게 자세히 공표하지 말자. 그래야 클라이언트가 이 값에 의지하지 않게 되고, 추후에 계산 방식을 바꿀 수도 있다.
'Essential Language Skill > Effective Java' 카테고리의 다른 글
Item 14. Comparable을 구현할지 고려하라 (0) | 2019.12.11 |
---|---|
Item 12. toString을 항상 재정의하라 (0) | 2019.12.11 |
Item 10. equals는 일반 규약을 지켜 재정의하라 (0) | 2019.12.08 |
Item 9. try-finally보다는 try-with-resource를 사용하라 (0) | 2019.12.08 |
Item 7. 다 쓴 객체 참조를 해제하라 (0) | 2019.12.08 |
- Total
- Today
- Yesterday
- JMM
- Effective Java
- POD
- JVM
- Delete
- Java
- kubernetes
- k8s
- Replication Controller
- 자바 메모리 구조
- Java Memory Structure
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |