Item 11. equals를 재정의하려거든 hashCode도 재정의하라

다람·2025년 3월 3일
0

Effective Java

목록 보기
11/13
post-thumbnail

1. 왜 equals를 재정의하면 hashCode도 재정의해야 할까?

  • equals를 재정의하면 같은 논리적 값을 가진 객체들은 서로 동일한 hashCode를 반환해야 한다.
  • hashCode를 재정의하지 않으면 않으면 HashMap, HashSet, HashTable 같은 해시 기반 컬렉션에서 동작이 제대로 이루어지지 않는다. 즉 동일한 객체라도 다른 해시값을 가질 수 있게된다는 뜻이다.
  • hashCode()를 재정의하지 않으면 기본적으로 ObjecthashCode()를 사용한다.
    • Object의 기본 hashCode()객체의 메모리 주소를 기반으로 생성되므로, equals()를 재정의해도 논리적으로 같은 객체가 다른 해시값을 가질 수 있다.

hashCode()를 재정의하지 않았을 때 발생하는 문제

import java.util.HashMap;

public class PhoneNumber {
    private final int areaCode;
    private final int prefix;
    private final int lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNumber = lineNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof PhoneNumber)) return false;
        PhoneNumber that = (PhoneNumber) o;
        return areaCode == that.areaCode &&
               prefix == that.prefix &&
               lineNumber == that.lineNumber;
    }

    // hashCode() 미구현

    public static void main(String[] args) {
        HashMap<PhoneNumber, String> map = new HashMap<>();
        PhoneNumber number1 = new PhoneNumber(123, 456, 7890);
        map.put(number1, "Alice");
        
        PhoneNumber number2 = new PhoneNumber(123, 456, 7890);
        System.out.println(map.get(number2)); // null이 출력됨
    }
}
  • 위 코드에서 number1number2같은 논리적 값을 가지지만 hashCode()가 없으므로 HashMap 내부에서 같은 객체로 인식하지 못해 null이 출력된다.

해결 방법 : hashCode() 재정의

@Override
public int hashCode() {
    return Objects.hash(areaCode, prefix, lineNumber);
}
  • Objects.hash()를 활용하면 간결하게 hashCode()를 재정의할 수 있다.

2. hashCode() 작성 요령

2-1. hashCode()는 같은 객체에 대해 항상 같은 값을 반환해야 한다.

  • 객체의 필드 값이 변하지 않는다면 hashCode()는 항상 같은 값을 반환해야 한다.
  • 동일한 애플리케이션 실행 중에는 같은 객체가 동일한 해시값을 가져야 한다.

2-2. equals()가 같은 두 객체는 반드시 같은 hashCode를 가져야 한다.

  • a.equals(b) == true이면 a.hashCode() == b.hashCode()여야 한다.
  • 하지만 다른 객체라도 같은 hashCode()를 가질 수 있긴하다.

2-3. 서로 다른 객체는 반드시 다른 hashCode()를 가질 필요는 없지만 다르면 좋다.

  • 해시 충돌이 적을수록 HashMap, HashSet 등의 성능이 좋아지기 때문이다.

2-4. hashCode를 구현하는 방법

  1. 기본적인 해시 계산 방식 (31을 사용한 해시 함수)

    @Override
    public int hashCode() {
        int result = Integer.hashCode(areaCode);
        result = 31 * result + Integer.hashCode(prefix);
        result = 31 * result + Integer.hashCode(lineNumber);
        return result;
    }
    • 31을 곱하는 이유 : 홀수이면서 소수이기 때문에 곱했을 때 해시 충돌을 줄일 수 있다.
    • 필드가 많을수록 해시값을 섞는 효과가 커진다.
  2. Objects.hash() 사용

    @Override
    public int hashCode() {
        return Objects.hash(areaCode, prefix, lineNumber);
    }
    • Objects.hash()를 사용하면 내부적으로 적절한 해시 함수를 사용해 필드 값을 결합해준다.
    • 대신 속도가 느리다는 단점이 있다.
  3. 해시 코드 캐싱 (불변 객체일 때 최적화 가능)

    private int hash; // 캐시된 해시값
    
    @Override
    public int hashCode() {
        if (hash == 0) {
            hash = Objects.hash(areaCode, prefix, lineNumber);
        }
        return hash;
    }
    • 불변 객체라면 해시값을 한 번 계산한 후 캐싱하여 성능을 최적화할 수 있다.
    • String 클래스도 같은 방식으로 hashCode()를 캐싱한다.

3. Google Guava와 AutoValue를 이용한 자동 생성

3-1. Google Guava의 Objects.hashCode() 사용

@Override
public int hashCode() {
    return com.google.common.base.Objects.hashCode(areaCode, prefix, lineNumber);
}
  • Guava 라이브러리를 사용하면 해시 코드를 자동으로 생성할 수 있다.
  • Objects.hashCode()와 비슷하지만 더 정교한 해싱 알고리즘을 사용한다.

4-2. AutoValue를 활용한 자동 생성

@AutoValue
abstract class PhoneNumber {
    abstract int areaCode();
    abstract int prefix();
    abstract int lineNumber();
    

    static PhoneNumber create(int areaCode, int prefix, int lineNumber) {
        return new AutoValue_PhoneNumberint areaCode, int prefix, int lineNumber);
    }
}
  • Google AutoValue를 사용하면 equals와 hashCode를 자동 생성할 수 있다.
  • @AutoValue를 붙이면 hashCode()를 직접 구현할 필요 없이 컴파일러가 자동으로 생성해 준다.
  • equals/hashCode 단위 테스트를 생략해도 된다.(item10에서와 동일한 내용)

4. 결론

  1. equals()를 재정의하면 hashCode()도 반드시 재정의해야 한다.
  2. 같은 객체라면 항상 같은 hashCode를 반환해야 한다.
  3. 해시 함수의 품질이 중요하며, 31을 사용한 해시 함수가 일반적으로 권장된다.
  4. Objects.hash()를 사용하면 간결하게 구현 가능하다.
  5. 불변 객체라면 hashCode를 한 번만 계산하고 캐싱하는 것도 좋은 방법이다.
  6. Google Guava, AutoValue를 사용하면 더욱 안전하고 최적화된 hashCode를 생성할 수 있다.
profile
개발하는 다람쥐

0개의 댓글