본문 바로가기

study/effective java

[item11] equals와 hashCode는 같이 정의해라

hashCode의 역할

hashCode는 객체의 해시코드를 반환해주는 책임을 수행하는 메소드이다. 해시라는 값은 같은 객체라면 같은 해시값을, 아니면 다른 해시값값을 반환해야야한다.

hashCode는 Set,HashMap 등의 Collections에서 해시를 직접 사용하는 객체에 영향을 줄 수 있다.

hashCode 규약

hashCode도 equals와 마찬가지지로 규약이 있다. JavaDocs로 작성된 Object명세에 다음과 같이 작성되어 있다.

  1. equals 를 통한 비교 정보가 달라지지 않았으면 hashCode도 변하지 않아야 한다. 단 어플리케이션이 재실행되면 값이 변할 수 있다.
  2. 두 객체의 equals의 결과가 true이면 같은 hashCode 를 가진다
  3. 두 객체의 equals의 결과가 false여도 서로 다른 hashCode를 표현할 필요는 없다. 단 다른값으로 표현해야만 hashtable의 성능이 좋아진다.

equals는 logical equality을 표현한다

item 10의 내용에서 equals 메소드를 오버라이딩하여 logical equality 를 표현할 때 사용한다. 즉 equals 메소드를 통해 물리적으론 다른 위치에 있는 두 객체라도 논리적으로 같은 객체로 파악할 수 있다.

즉 equals 메소드의 결과가 true인 두 객체는 논리적으로 같은 객체임을 의미한다.

equals만 정의하고 hashCode를 정의하지 않는 경우

원래 hashCode를 해부하여 보고 싶었으나 JNI로 구현되어 있어 보지 못했다.

equals 를 통해 logical equality를 표현하여도 hashCode가 다르다면 HashMap, HashSet같은 hashCode에 의존되는 객체를 사용할 때 논리적으로 같은 객체임에도 다른 객체로 판별되어 객체가 저장되어 있음에도 찾지 못하는 문제가 발생할 수 있다.

hashCode작성법

hashCode는 다음의 방법을 유의해서 작성해야 한다.

  1. equals 메소드에 사용되는 모든 필드들을 이용하여 hash를 계산한다.
  2. hash결과는 int 범위 내어야 한다.
  3. 서로 다른 두 객체는 서로 다른 hash를 가져야 한다.
  4. hashCode의 생성 규칙은 자세히 설명하지 말자
    • hashCode에 버그가 발견되었거나 더 성능이 좋은 방식이 있음에도 불구하고 이미 공표가 되어버린 hashCode 생성 로직을 변경할 시 이를 의존하는 다른 기능들에 문제가 발생하기에 바꾸기가 어려워지는 문제를 방지하자는 의미이다.

1. 소수 X result

hash결과를 누적하는 result에 대하여 아래의 식으로 result를 누적한다.

result=필드의 해시값 X result X 소수

소수는 31를 이용하는 경우가 많다. String 객체에서는 hashCode를 다음과 같은 결과롤 반환한다고 JavaDocs에 작성되어 있다.

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

각 원소에 대해 31를 곱하고 더하는 방식으로 이용한다.

왜 많은 소수중에 31일까에 의문에 대해서 effective java 는 다음과 같이 설명한다.

  1. 31은 홀수이며 소수이다.(메르센 소수)
  2. 시프트 연산과 뺏셈을 하나의 연산으로 대체할 수 있다.
    • 31* i == (i << 5) - i

2번의 경우는 VM이 성능이 좋아 최적화를 잘 해준다고 설명한다.

물론 모든 hash 계산에서 31이 적합한 소수는 아니다. 스택 오버플로우 커뮤니티에도 이에 대한 논쟁이 있다. What is a sensible prime for hashcode calculation?

2. Objects.hash

Objects.hash() : 매개변수로 받은 객체들을 통해 hash를 계산해주는 메소드.

intelij 의 Generate기능인 equals and hashcode 를 이용할 시 이 메소드를 이용하게 된다.

public class Point {

    private final int y;
    private final int x;

    // 생성자, equals 생략

    @Override
    public int hashCode() {
        return Objects.hash(y, x);
    }
}

이 메소드를 사용하면 hash계산을 쉽게 할 수 있느나 파라미터 이동간 boxing, unboxing이 발생하기에 성능 저하의 문제가 발생할 수 있다.


결론

equals는 두 객체가 같다는 것을 의미하므로 같은 객체면 같은 값을 반환해야 하는 hashCode도 같은 결과를 내도록 재작성해야한다는 의미이다.


Reference