본문 바로가기
Language/Java

이펙티브 자바 - hashCode 재정의

by ocwokocw 2021. 10. 2.

- 이 글은 Effective Java 를 기반으로 작성되었습니다.

- hashCode 재정의의 중요성

equals 를 재정의할 때는 반드시 hashCode 도 재정의 해야 한다. 그러지 않으면 HashSet, HashMap, HashTable 과 같은 Hash 컬렉션을 사용할 때 문제가 발생할 수 있다.

 

Object 클래스 명세에서 hashCode 에 관한 일반 규약을 살펴보자.

  • equals 가 사용하는 정보가 변경되지 않는다면 hashCode 를 여러 번 호출해도 동일 hash 값을 반환해야 한다.
  • equals 메소드가 같다고 판정한 두 객체는 같은 hash 값을 반환해야 한다.
  • equals 메소드가 다르다고 판정한 두 객체의 hash 값이 같을 수는 있다. 하지만 이런 일이 자주일어나면 성능이 떨어진다는점은 알고 있어야 한다.

- 규약의 위반

위의 3 가지 일반 규약 중 hashCode 를 재정의 하지 않았을 때, 문제가 되는 핵심규약은 2 번째 규약이다. 어떤 클래스의 속성 값이 모두 같은 2 객체가 있다고 가정해보자. 이 속성들이 같을 때 논리적으로 같다고 판정하고 싶다면 equals 메소드를 재정의할 것이다. 하지만 hashCode 를 재정의하지 않으면 객체의 hash 값은 다른값을 반환한다.

 

public class PhoneNumber {

	private final short areaCode;
	private final short prefix;
	private final short lineNumber;
	
	public PhoneNumber(int areaCode, int prefix, int lineNumber) {
		super();
		rangeCheck(areaCode, 999, "area code");
		rangeCheck(prefix, 999, "prefix");
		rangeCheck(lineNumber, 9999, "line number");
		
		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 obj) {
		if (this == obj)
			return true;
		if (!(obj instanceof PhoneNumber))
			return false;
		
		PhoneNumber other = (PhoneNumber) obj;
		
		return areaCode == other.areaCode &&
			lineNumber == other.lineNumber &&
			prefix == other.prefix;
	}
}

 

위와 같이 PhoneNumber 클래스가 있다. areaCode, lineNumber, prefix 가 같을 시 동등하다 라고 정의하기 위해 equals 메소드를 재정의하고 hashCode 는 정의하지 않았다.

 

public class HashTest {

	public static void main(String[] args) {

		PhoneNumber phoneNumber1 = new PhoneNumber(123, 123, 1234);
		PhoneNumber phoneNumber2 = new PhoneNumber(123, 123, 1234);
		
		System.out.println("phoneNumber1.equals(phoneNumber2): " + phoneNumber1.equals(phoneNumber2));
		System.out.println("phoneNumber1 hash: " + phoneNumber1.hashCode());
		System.out.println("phoneNumber2 hash: " + phoneNumber2.hashCode());
	}
}

.....

phoneNumber1.equals(phoneNumber2): true
phoneNumber1 hash: 705927765
phoneNumber2 hash: 366712642

 

phoneNumber1 과 phoneNumber2 는 같지만 각 객체의 hash 값을 출력해보면 다른것을 알 수 있다. 이게 문제가 될까? equals 만 같다고 판단하면 문제가 없는것 아닌가? 문제는 Hash 기반의 Collection 을 사용할 때 나타난다.

 

Map<PhoneNumber, String> phoneBook = new HashMap<>();

phoneBook.put(phoneNumber1, "A");

System.out.println("phoneBook.containsKey(phoneNumber2): " + phoneBook.containsKey(phoneNumber2));

......

phoneBook.containsKey(phoneNumber2): false

 

HashMap 에 put 을 할 때에는 phoneNumber1을 key 로, 포함되어있는지 검사할때에는 phoneNumber2 로 검사를 하였다. 다른 객체이긴 하지만 equals 메소드로 객체의 속성값이 모두 같으면 동등하다고 정의했으므로 key 가 포함되어있다고 나와야 한다. 하지만 결과는 전혀 다르다.


- hashCode 정의

hashCode 를 정의해보자. hashCode 는 int 형을 반환하는 메소드 시그니처를 갖는다. 아래와 같이 그냥 단일값으로 반환하면 될까?

 

@Override
public int hashCode() {
	return 1;
}

 

hashCode 가 반환하는 값은 equals 에 정의한 Field 들을 기준으로 균등한 값을 가질수록 성능이 좋아진다. 위와 같은 메소드의 경우 모든 객체가 같은 값을 가지게 되어 O(1) 의 시간을 기대하고 쓰는 Hash 라는 자료구조의 이점을 전혀 살리지 못하고 O(n) 이 되어버린다.

 

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

 

Hash 값을 어떻게 생성해야 균등한지 분포도가 좋은지는 알기 어려우며 거의 수학적 영역에 가깝다. 다행스럽게도 보통 IDE 에서 hashCode 메소드를 자동으로 생성해준다.


- 그 외 고려사항들

hashCode 함수 재정의시 추가적으로 고려해야할 몇 가지 사항들을 알아보자.

 

첫째로 다른값에서 파생될 수 있는 값은 무시할 수 있다. hashCode 는 해당 객체의 동등성에 대해 최대한 유일값을 반환해야 한다. 다른값에서 파생된다는 건 중복 정보 이므로 이를 고려할 필요 없다.

 

둘째로 equals 에 쓰이지 않는 Field 는 반드시 제거해야 한다. 객체의 동등성을 판별하는데 a, b 속성만 사용되는데 hash 값을 생성할때에 c 속성까지 넣어버리면 a와 b가 같아 동등한 객체여도 c값이 다르면 다른 hash 를 반환한다. 이는 단순한 성능저하를 넘어서 hashCode 의 규약을 깨는것이기 떄문에 반드시 지켜줘야 한다.

 

셋째로 성능 개선을 위해 hashCode 값 계산에서 주요 속성을 제거할때에는 신중해야 한다. 만약 제거하기로 한 해당속성값만 다른 객체가 아주 많이 생기면 성능상 불이익을 받을 수 있기 때문이다.

댓글