Effective Java. 09. equals 메소드를 오버라이드 할 때는 hashCode 메소드도 항상 같이 오버라이드 하자

Jae·2024년 3월 31일
0

Effective Java

목록 보기
10/11

equals메소드를 오버라이드하게되면 반드시 hashcode도 오버라이딩 해줘야한다.
이는 규칙이며, 하지 않게되면 HashMap, HashSet 등 Hash기반의 컬렉션들과 개발한 클래스들을 사용할 때 제대로 작동하지 않을 것이다.

hashCode 규칙

Java API 내에서도 명시되어 있다.

The general contract of hashCode is:

  • Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
  • If two objects are equal according to the equals method, then calling the hashCode method on each of the two objects must produce the same integer result.
  • It is not required that if two objects are unequal according to the equals method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
  • Java 애플리케이션의 실행중에 동일한 객체에 대해 한 번 이상의 호출되더라도 hashCode 메소드는 같은 정수를 일관성 있게 반환해야한다.
  • equals 메소드 호출 결과 두 객체가 동일하다면, 두 객체 각각에 대해 hashCode메소드를 호출했을 때 같은 정수 값이 나와야 한다.
  • 두 객체가 같은 메서드에 대한 함수값이 동일하지 않다고 해서, 두 객체가 hashCode를 호출 했을 때 같을 필요는 없다. 그러나 같지 않은 객체들에 대해 hascCode메소드에서 서로 다른 정수 값을 반환하면, 이 메소드를 사용하는 해시 컬렉션들의 성능을 향상시킬 수 있다.

오버라이딩에 실패했을 때 위배되는 주요 조항은 두 번째 조항이다. 두 객체가 있을 때, equals를 통해서 두 객체가 논리적으로 동일한 지 알 수 있다 hashCode는 두 객체가 논리적으로 동일한지 알 수 없다.

import java.util.HashMap;
import java.util.Map;

public class PhoneNumber {

  private final short areaCode;
  private final short prefix;
  private final short lineNumber;

  public PhoneNumber(short areaCode, short prefix, short lineNumber) {

    rangeCheck(areaCode, 999, "area code");
    rangeCheck(prefix, 999, "prefix");
    rangeCheck(lineNumber, 9999, "line number");
    this.areaCode = areaCode;
    this.prefix = prefix;
    this.lineNumber = lineNumber;
  }

  private void rangeCheck(short arg, int max, String name) {
    if (arg < 0 || arg > max) {
      throw new IllegalArgumentException(name + ": " + arg);
    }
  }

  @Override
  public boolean equals(Object obj) {

    if (obj == this) {
      return true;
    }

    if (!(obj instanceof PhoneNumber)) {
      return false;
    }
    PhoneNumber pn = (PhoneNumber) obj;

    return pn.lineNumber == lineNumber
        && pn.prefix == prefix
        && pn.areaCode == areaCode;

  }

  @Override
  public int hashCode() {
    return super.hashCode();
  }

  public static void main(String[] args) {
    Map<PhoneNumber, String> m = new HashMap<>();
    m.put(new PhoneNumber((short) 707, (short) 867, (short) 5309), "jeong");

    System.out.println(m.get(new PhoneNumber((short) 707, (short) 867, (short) 5309)));
  }
}

이와 같은 코드에서 결과값은 true일 거 같지만 null을 리턴한다.
왜냐면 new를 이용해서 새로운 인스턴스를 만들기 때문에 hashCode에서 메소드를 오버라이드하지않아서 두 인스턴스가 서로 다른 해시 코드 값을 갖게된 것이다.
이러한 부분은 hashcode 메소드를 오버라이딩해서 원하는 값을 출력할 수 있다.

이해한 바로는 위의 코드의 경우 HashMap, HashSet과 같은 자료구조에서 의미상으로 들어간 데이터(new PhoneNumber((short) 707, (short) 867, (short) 5309))이 동일한 value를 갖고 있는데, 이 값의 인스턴스가 달라 값을 가져오지 못하는 문제로 인해 잘못된 상황이다를 얘기하는 것 같고 이 문제는 hashcode 메서드의 오버라이딩을 통해 해결 가능하다 같다.

hashCode 메서드 만들기

  1. 0이 아닌 상수값을 result 에 정리한다. 이 때 상수값은 소수가 좋다.

  2. 객체의 equals에서 비교하는 주요 필드 변수 f에 대해 다음과 같이 수행한다.
    필드에 대한 int 타입의 해시 코드 c를 다음과 같이 산출한다.

    • 필드 f가 boolean 타입이면, (f? 1 : 0)
    • 필드 f가 byte, char, short, int 타입이면, (int) f
    • 필드 f가 long 타입이면, (int) (f^ (f >>> 32))
    • 필드 f가 float 타입이면, Float.floatToIntBits(f)
    • 필드 f가 double 타입이면, Double.doubleToLongBits(f)를 실행한 후 반환된 long 타입을 (int) (f^ (f >>> 32)) 이와 같이 처리
    • 필드 f가 객체 참조일 경우는 현재 객체의 equals메소드에 그 필도를 비교하기 위해 f가 참조하는 객체의 equals메소드를 재귀적으로 호출한다.
    • 만일 더 복잡한 비교가 필요하다면, 필드의 표준 형식을 만들어서 처리하고, 그 표준 형식에 대해 hashcode 메소드를 호출한다. 그 필드 값이 null이라면 0을 반환한다.
    • 필드 f가 배열이라면 배열의 각 요소를 별개의 필드처럼 처리한다.

    위의 단계에서 구현한 해시코드 c를 result에 합산한다.

  3. result를 반환한다.

  4. hashcode의 메소드 작성이 끝나면 동일한 인스턴스들이 같은 값을 갖는지 검토하고 잘못된 점이 있으면 수정하여 완료한다.

규칙에 따라 구현하기

해시코드 산출시 파생 필드는 제외시킬 수 있는데, 이 말은 해시코드 연산에 포함되는 필드로부터 값이 산출될 수 있는 어떤 필드도 무시할 수 있다고 한다. equals 메소드에서 비교하지 않는 필드는 제외시켜야한다. 그렇지 않으면 2번째 조항을 어길 수 있기 때문이다.

따라서 아래와 같이 바꿀 수 있다.

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

이렇게 바꾸면 true가 나오게 된다.
불변이면서 해시 코드 연산이 중요한 클래스라면 해시 값을 매번 연산하는 것이 아니라, 객체 내부에 해시 코드를 저장하는 것을 고려해 볼 수 있다. 이런 경우엔 인스턴스가 생성될 때 해시 코드를 산출해야하고, 그게 아니라면, 늦초기화를 통해 할 수도 있다.

주의 사항

해시 코드를 산출할 때 성능 향상을 이유로 객체의 중요 부분을 제외시키지 말자. 품질 저하로 인해 해시 테이블의 성능을 떨어뜨릴 수 있다.

참고 문헌

Effective Java ed2

0개의 댓글