Hyeonuk_.log

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

Dev_.log/Java

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

Hyeonuk_. 2022. 2. 25. 06:00

 

https://book.naver.com/bookdb/book_detail.nhn?bid=8064518 

 

이펙티브 자바

JAVA, 더 깔끔하고, 정확하고, 안전하며, 재사용이 쉬운 코드로 만들자!『EFFECTIVE JAVA(이펙티브 자바)』는 효율적이면서도 잘 설계된 자바 프로그램을 구현하기 위해 알아야 할 지침 78가지를 소개

book.naver.com

 

이펙티브 자바 2판, 규칙9를 읽고 정리를 하려 한다. 규칙 9에서는 hashCode 메서드에 대해 말해주고 있다.

equals 메서드를 재정의하는 클래스는 hashCode 메서드도 반드시 재정의 해야한다.

 

 

Object 클래스 명세에서는 아래와 같이 hashCode 규약을 말하고 있다.

- 응용프로그램 실행 중에 같은 객체의 hashCode를 여러 번 호출하는 경우, equals가 사용하는 정보들이 변경되지 않았다면, 언제나 동일한 정수(interger)가 반환되어야 한다. 다만, 종료되었다가 다시 실행되어도 같은 값이 나올 필요는 없다.

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

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

 

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,     9999, "prefix");
		rangeCheck(lineNumber, 9999, "lineNumber");
		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 == this) return true;
		if(!(o instanceof PhoneNumber)) return false;
		
		PhoneNumber pNumber = (PhoneNumber) o;
		return pNumber.areaCode == areaCode
				&& pNumber.prefix == prefix
				&& pNumber.lineNumber == lineNumber;
	}
	
	// hashCode가 없다면 문제가 된다!!!!
	public static void main(String argsp[]) {
		Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>();
		map.put(new PhoneNumber(707, 867, 5309), "Jenny");
		
		System.out.println(map.get(new PhoneNumber(707, 867, 5309)));  // null 리턴됨.
	}
	
}


아래의 코드 추가 후에는 정상적으로 Jenny를 출력한다.

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

 

근데 왜 17이 있고 왜 31이 있을까???

책에서 말하는 지침들이 있다.

1. 17과 같은 0이 아닌 상수를 result라는 이름의 int 변수에 저장한다. (17은 임의의 값)
2. 객체 안에 있는 모든 중요 필드 f에 대해서(equals가 사용하는 필드) 아래의 절차를 시행한다.
    A. 해당 필드에 대한 int 해시코드 c를 계산한다. (각 타입별로 방법이 기재되어 있다. 필요할 때 참고)
    B. 위의 절차 A에서 계산된 해시코드 c를 result에 다음과 같이 결합한다.
    result = 31 * result + c (31은 소수이면서 홀수이기에 선택, 만약 짝수였고 곱셈의 결과가 오버플로 되었다면 정보는 사라졌을 것. 2로 곱하는 것은 비트를 왼쪽으로 시프트하는 것과 같기 때문이다.)
3. result를 반환한다.

 

Comments