컬렉션의 hashCode 메서드에 대한 최상의 구현


299

hashCode()컬렉션에 가장 적합한 메서드 구현을 결정하려면 어떻게해야합니까 (메소드와 같은 것이 올바르게 재정의되었다고 가정)?


2
Java 7 이상에서는 Objects.hashCode(collection)완벽한 솔루션이어야합니다!
디아블로

3
@Diablo 나는 그 질문에 전혀 답하지 않는다고 생각하지 않습니다-그 방법은 단순히 collection.hashCode()( hg.openjdk.java.net/jdk7/jdk7/jdk/file/9b8c96f96a0f/src/share/… ) 를 반환합니다.
cbreezier

답변:


438

최고의 구현? 사용법 패턴에 따라 다르므로 어려운 질문입니다.

거의 모든 경우에 합리적인 구현이 Josh Bloch효과적인 Java 항목 8 (제 2 판)에서 제안되었습니다 . 가장 좋은 방법은 저자가 접근 방식이 좋은 이유를 설명하기 때문에 거기서 찾아 보는 것입니다.

짧은 버전

  1. a를 작성하고 0이 아닌 값을 int result지정 하십시오 .

  2. 들어 모든 분야 f 에서 테스트 equals()방법의 해시 코드를 계산 c하여 :

    • 필드 f가 a 인 경우 boolean: 계산 (f ? 0 : 1);
    • 필드 F 인 경우 byte, char, short또는 int: 계산 (int)f;
    • 필드 f가 a 인 경우 long: 계산 (int)(f ^ (f >>> 32));
    • 필드 f가 a 인 경우 float: 계산 Float.floatToIntBits(f);
    • 필드 f가 a 인 경우 double: Double.doubleToLongBits(f)모든 긴 값처럼 리턴 값을 계산 하고 처리하십시오.
    • 필드 f가 객체 인 경우 : hashCode()메소드 의 결과를 사용 하거나 0이면 f == null;
    • 필드 f가 배열 인 경우 : 모든 필드를 별도의 요소로보고 해시 값을 재귀적인 방식으로 계산하고 다음에 설명 된대로 값을 결합하십시오.
  3. 해시 값 cresult다음 과 결합하십시오 .

    result = 37 * result + c
  4. 반환 result

이로 인해 대부분의 사용 상황에 적절한 해시 값이 배포됩니다.


45
네, 특히 37 번이 어디에서 왔는지 궁금합니다.
Kip

17
Josh Bloch의 "Effective Java"책의 항목 8을 사용했습니다.
dmeister

39
@dma_k 소수를 사용하는 이유와이 답변에 설명 된 방법은 계산 된 해시 코드가 고유 하도록하기위한 입니다. 비 프라임 숫자를 사용하는 경우이를 보장 할 수 없습니다. 어떤 소수를 선택하든 상관 없습니다. 37 번에 대해서는 마법이 없습니다 (너무 나쁜 42는 소수가 아닙니다.)
Simon Forsberg

34
@ SimonAndréForsberg 글쎄, 계산 된 해시 코드는 항상 고유 할 수는 없습니다 :) 해시 코드입니다. 그러나 나는 아이디어를 얻었다 : 소수는 오직 하나의 승수를 가지고 있지만 비 프라임은 적어도 2를 가진다. 이는 곱셈 연산자가 동일한 해시를 생성하도록 충돌을 일으키는 추가 조합을 만듭니다.
dma_k

14
나는 Bloch 가 최적화의 용이성을 위해 37이 아닌 31을 곱 한다고 생각한다 .
ruffin

140

dmeister가 권장하는 효과적인 Java 구현에 만족하면 직접 롤링하는 대신 라이브러리 호출을 사용할 수 있습니다.

@Override
public int hashCode() {
    return Objects.hashCode(this.firstName, this.lastName);
}

여기에는 Guava ( com.google.common.base.Objects.hashCode) 또는 Java 7 ( java.util.Objects.hash) 의 표준 라이브러리가 필요 하지만 같은 방식으로 작동합니다.


8
이것을 사용하지 않는 정당한 이유가없는 한, 반드시 이들을 사용해야합니다. 표준 구현 / 라이브러리를 사용하기위한 일반적인 주장이 적용됩니다 (모범 사례, 잘 테스트되고 오류가 적은 경향 등).
사키

7
@ justin.hughey 혼란스러워하는 것 같습니다. 재정의해야 할 유일한 경우 hashCode는 custom이있는 경우 equals이며, 이러한 라이브러리 메소드가 설계된 것입니다. 설명서와 관련하여 해당 동작이 명확합니다 equals. 라이브러리 구현은 올바른 hashCode구현 의 특성이 무엇인지 알지 못한다고 주장하지 않습니다. 이러한 라이브러리를 사용하면 재정의 되는 대부분의 경우에 적합한 구현을 쉽게 구현할 수 equals있습니다.
bacar

6
java.util.Objects 클래스를보고있는 Android 개발자의 경우 API 19에서만 소개되었으므로 KitKat 이상에서 실행 중인지 확인하십시오. 그렇지 않으면 NoClassDefFoundError가 발생합니다.
앤드류 켈리

3
모범 답변 IMO, 예를 들어 java.util.Objects.hash(...)구아바 com.google.common.base.Objects.hashCode(...)방법 보다 JDK7 방법 을 선택했을 것 입니다. 나는 대부분의 사람들이 추가적인 의존성보다 표준 라이브러리를 선택할 것이라고 생각합니다.
Malte Skoruppa

2
이 두 개의 인수 이상이며, 그 중 하나가 배열 인 경우, 결과가있을 경우 때문에 기대하지 어떤 hashCode()배열에 대한 바로 그 것이다 java.lang.System.identityHashCode(...).
starikoff

59

꽤 좋은 일을하는 Eclipse가 제공하는 기능을 사용하는 것이 좋으며 비즈니스 로직을 개발하는 데 노력과 에너지를 투입 할 수 있습니다.


4
+1 좋은 실용적인 솔루션. dmeister의 솔루션은보다 포괄적이지만 해시 코드를 직접 작성하려고 할 때 null을 처리하는 것을 잊어 버리는 경향이 있습니다.
Quantum7

1
+1 Quantum7에 동의하지만 Eclipse 생성 구현이 수행하는 작업과 구현 세부 정보를 얻는 위치를 이해하는 것이 좋습니다.
jwir3

15
죄송하지만 "[일부 IDE]에서 제공하는 기능"과 관련된 답변은 일반적으로 프로그래밍 언어와 관련이 없습니다. 수십 개의 IDE가 있으며 이것은 질문에 대답하지 않습니다. 즉, 알고리즘 결정에 관한 것이고 equals () 구현과 직접 관련되어 있기 때문에 IDE가 알지 못하는 것입니다.
Darrell Teague

57

이것은 Android문서 (Wayback Machine)Github의 내 코드 와 연결되어 있지만 일반적으로 Java에서 작동합니다. 내 대답은 읽고 이해하기 훨씬 쉬운 코드 로 dmeister의 답변 을 확장 한 것입니다.

@Override 
public int hashCode() {

    // Start with a non-zero constant. Prime is preferred
    int result = 17;

    // Include a hash for each field.

    // Primatives

    result = 31 * result + (booleanField ? 1 : 0);                   // 1 bit   » 32-bit

    result = 31 * result + byteField;                                // 8 bits  » 32-bit 
    result = 31 * result + charField;                                // 16 bits » 32-bit
    result = 31 * result + shortField;                               // 16 bits » 32-bit
    result = 31 * result + intField;                                 // 32 bits » 32-bit

    result = 31 * result + (int)(longField ^ (longField >>> 32));    // 64 bits » 32-bit

    result = 31 * result + Float.floatToIntBits(floatField);         // 32 bits » 32-bit

    long doubleFieldBits = Double.doubleToLongBits(doubleField);     // 64 bits (double) » 64-bit (long) » 32-bit (int)
    result = 31 * result + (int)(doubleFieldBits ^ (doubleFieldBits >>> 32));

    // Objects

    result = 31 * result + Arrays.hashCode(arrayField);              // var bits » 32-bit

    result = 31 * result + referenceField.hashCode();                // var bits » 32-bit (non-nullable)   
    result = 31 * result +                                           // var bits » 32-bit (nullable)   
        (nullableReferenceField == null
            ? 0
            : nullableReferenceField.hashCode());

    return result;

}

편집하다

일반적으로을 무시하면을 (를) 재정의 hashcode(...)하려고합니다 equals(...). 따라서 이미 구현했거나 이미 구현 한 사람들을 위해 equals여기 Github에서 좋은 참조가 있습니다 ...

@Override
public boolean equals(Object o) {

    // Optimization (not required).
    if (this == o) {
        return true;
    }

    // Return false if the other object has the wrong type, interface, or is null.
    if (!(o instanceof MyType)) {
        return false;
    }

    MyType lhs = (MyType) o; // lhs means "left hand side"

            // Primitive fields
    return     booleanField == lhs.booleanField
            && byteField    == lhs.byteField
            && charField    == lhs.charField
            && shortField   == lhs.shortField
            && intField     == lhs.intField
            && longField    == lhs.longField
            && floatField   == lhs.floatField
            && doubleField  == lhs.doubleField

            // Arrays

            && Arrays.equals(arrayField, lhs.arrayField)

            // Objects

            && referenceField.equals(lhs.referenceField)
            && (nullableReferenceField == null
                        ? lhs.nullableReferenceField == null
                        : nullableReferenceField.equals(lhs.nullableReferenceField));
}

1
Android 문서에는 이제 위의 코드가 더 이상 포함되어 있지 않으므로 Wayback Machine
-Android

17

먼저 equals가 올바르게 구현되었는지 확인하십시오. 에서 IBM의 developerWorks 기사 :

  • 대칭 : 두 참조 a와 b의 경우 b.equals (a) 인 경우에만 a.equals (b)
  • 반사성 : null이 아닌 모든 참조의 경우 a.equals (a)
  • 전이성 : aequals (b) 및 b.equals (c)이면 a.equals (c)

그런 다음 hashCode와의 관계가 동일한 기사의 연락처를 존중하는지 확인하십시오.

  • hashCode ()와의 일관성 : 두 개의 동일한 객체는 동일한 hashCode () 값을 가져야합니다.

마지막으로 좋은 해시 함수는 이상적인 해시 함수 에 접근하기 위해 노력해야 합니다 .


11

about8.blogspot.com, 당신은 말했다

두 객체에 대해 equals ()가 true를 반환하면 hashCode ()는 동일한 값을 반환해야합니다. equals ()가 false를 반환하면 hashCode ()는 다른 값을 반환해야합니다

나는 당신에 동의 할 수 없습니다. 두 객체에 동일한 해시 코드가있는 경우 동일한 해시 코드를 의미 할 필요는 없습니다.

A가 B와 같으면 A.hashcode는 B.hascode와 같아야합니다.

그러나

A.hashcode가 B.hascode와 같다고해서 A가 B와 같아야한다는 의미는 아닙니다.


3
인 경우 (A != B) and (A.hashcode() == B.hashcode())해시 함수 충돌이라고합니다. 해시 함수의 공동 도메인은 항상 유한하기 때문에 도메인은 일반적으로 그렇지 않기 때문입니다. 코 도메인이 클수록 충돌 발생 빈도가 줄어 듭니다. 좋은 해시 함수는 특정 코 도메인 크기에서 가장 큰 가능성을 가지고 다른 객체에 대해 다른 해시를 반환해야합니다. 그래도 완전히 보장되지는 않습니다.
Krzysztof Jabłoński

이것은 위의 회색 게시물에 대한 주석이어야합니다. 정말 질문에 대답하지 않고 좋은 정보
크리스토퍼 Rucinski

equals () 및 hashCode () 구현은 반드시 OO 컨텍스트에서 서로 다른 객체에 대한 것이 아니라 일반적으로 도메인 모델 표현에 더 가깝기 때문에 좋은 의견이지만 '다른 객체'라는 용어 사용에주의하십시오. 사람들이 국가 코드와 국가 ID를 공유하면 동일하게 간주 될 수 있습니다. JVM에서 두 개의 서로 다른 '객체'일 수 있지만 '동일'한 것으로 간주되며 주어진 해시 코드를 갖는 것으로 간주됩니다.)
Darrell Teague

7

이클립스를 사용하면 다음을 생성 equals()하고 hashCode()사용할 수 있습니다 .

소스-> hashCode () 및 equals () 생성

이 함수 를 사용하면 동등성 및 해시 코드 계산에 사용할 필드 를 결정할 수 있으며 Eclipse는 해당 메소드를 생성합니다.


7

의 좋은 구현에있어 효과적인 자바hashcode()equals()의 논리 아파치 코 몬즈 랭 . 체크 아웃 HashCodeBuilderEqualsBuilder .


1
이 API의 단점은 equals 및 hashcode를 호출 할 때마다 (객체가 불변이고 해시를 사전 계산하지 않는 한) 객체 생성 비용을 지불한다는 것입니다.
James McMahon 2012

이것은 최근까지 내가 가장 좋아하는 접근법이었습니다. SharedKey OneToOne 연결에 대한 기준을 사용하는 동안 StackOverFlowError가 발생했습니다. 또한 Objects클래스는 Java7의 메소드 hash(Object ..args)equals()메소드를 제공합니다 . jdk 1.7 이상을 사용하는 모든 응용 프로그램에 권장됩니다
Diablo

@Diablo 나는 당신의 문제가 객체 그래프의 사이클이라고 생각합니다. 그런 다음 참조를 무시하거나 사이클을 중단해야하기 때문에 대부분의 구현에서 운이 좋지 않습니다 ( IdentityHashMap). FWIW ID 기반 hashCode를 사용하고 모든 엔티티에 동일합니다.
maaartinus

6

다른 더 자세한 답변을 완성하기위한 간단한 참고 사항 (코드 측면에서) :

내가 질문을 고려하면 방법 - 할 - 내가 만들 -에 - 자바 A-해시 테이블 특히 jGuru FAQ를 , 내가 해시 코드를 판단 할 수있는에 따라 다른 기준이 있습니다 생각 :

  • 동기화 (알고는 동시 액세스를 지원합니까?)
  • 안전 반복 실패 (알고가 반복 중에 변경되는 콜렉션을 감지 함)
  • 널값 (해시 코드가 콜렉션에서 널값을 지원함)

4

질문을 올바르게 이해하면 사용자 정의 컬렉션 클래스 (즉, Collection 인터페이스에서 확장되는 새 클래스)가 있고 hashCode () 메서드를 구현하려고합니다.

컬렉션 클래스가 AbstractList를 확장한다면 걱정할 필요가 없습니다. 모든 객체를 반복하고 hashCodes ()를 함께 추가하여 작동하는 equals () 및 hashCode () 구현이 이미 있습니다.

   public int hashCode() {
      int hashCode = 1;
      Iterator i = iterator();
      while (i.hasNext()) {
        Object obj = i.next();
        hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
      }
  return hashCode;
   }

이제 원하는 것이 특정 클래스의 해시 코드를 계산하는 가장 좋은 방법이라면 일반적으로 ^ (비트 배타적 또는 연산자) 연산자를 사용하여 equals 메소드에서 사용하는 모든 필드를 처리합니다.

public int hashCode(){
   return intMember ^ (stringField != null ? stringField.hashCode() : 0);
}

2

@ about8 : 거기에 꽤 심각한 버그가 있습니다.

Zam obj1 = new Zam("foo", "bar", "baz");
Zam obj2 = new Zam("fo", "obar", "baz");

동일한 해시 코드

당신은 아마 같은 것을 원할 것입니다

public int hashCode() {
    return (getFoo().hashCode() + getBar().hashCode()).toString().hashCode();

(요즘 Java에서 int에서 hashCode를 직접 얻을 수 있습니까? 자동 캐스팅을 수행한다고 생각합니다. 그런 경우 toString을 건너 뛰십시오.


3
이 버그는 about8.blogspot.com의 긴 대답에 있습니다. 문자열을 연결하여 해시 코드를 가져 오면 동일한 문자열에 추가되는 문자열 조합에 대해 동일한 해시 함수가 생깁니다.
SquareCog

1
이것은 메타 토론이며 전혀 질문과 관련이 없습니까? ;-)
Huppie

1
상당히 심각한 결함이있는 제안 된 답변에 대한 수정입니다.
SquareCog

이것은 매우 제한 구현
크리스토퍼 Rucinski

구현은 문제를 피하고 또 다른 문제를 소개합니다. 스와핑 foobar동일 hashCode합니다. 귀하의 toStringAFAIK는 컴파일되지 않습니다, 그것은 않을 경우, 그것은 비효율적 끔찍. 같은 뭔가가 109 * getFoo().hashCode() + 57 * getBar().hashCode()빠르고, 간단하고 더 불필요한 충돌을 생산하지 않습니다.
maaartinus

2

컬렉션에 대해 구체적으로 요청한대로 다른 답변에서 아직 언급하지 않은 측면을 추가하고 싶습니다. HashMap은 컬렉션에 추가 된 키가 해시 코드를 변경하지 않을 것으로 예상합니다. 모든 목적을 이길 것입니다 ...


2

Apache Commons EqualsBuilderHashCodeBuilder 에서 리플렉션 메소드를 사용하십시오 .


1
이것을 사용하려면 반사가 비싸다는 것을 명심하십시오. 나는 솔직히 코드를 버리는 것 외에는 이것을 사용하지 않을 것입니다.
James McMahon 2012

2

Arrays.deepHashCode(...)매개 변수로 제공된 배열을 올바르게 처리하기 때문에 작은 래퍼를 사용 합니다.

public static int hash(final Object... objects) {
    return Arrays.deepHashCode(objects);
}


1

코드를 깨끗하게 유지하는 데 도움이 되는 클래스 객체의 Google 컬렉션 라이브러리에서 유틸리티 메소드를 사용 하는 것이 좋습니다. 매우 자주 equals그리고 hashcode메소드는 IDE의 템플릿으로 만들어 지므로 읽기가 깨끗하지 않습니다.


1

다음은 슈퍼 클래스 로직을 고려한 또 다른 JDK 1.7+ 접근 데모입니다. Object 클래스 hashCode () 계정, 순수한 JDK 종속성 및 추가 수동 작업이없는 것이 매우 편리합니다. 참고하시기 바랍니다 Objects.hash()널 (null) 허용입니다.

나는 어떤 equals()구현도 포함하지 않았지만 실제로는 당신이 그것을 필요로 할 것입니다.

import java.util.Objects;

public class Demo {

    public static class A {

        private final String param1;

        public A(final String param1) {
            this.param1 = param1;
        }

        @Override
        public int hashCode() {
            return Objects.hash(
                super.hashCode(),
                this.param1);
        }

    }

    public static class B extends A {

        private final String param2;
        private final String param3;

        public B(
            final String param1,
            final String param2,
            final String param3) {

            super(param1);
            this.param2 = param2;
            this.param3 = param3;
        }

        @Override
        public final int hashCode() {
            return Objects.hash(
                super.hashCode(),
                this.param2,
                this.param3);
        }
    }

    public static void main(String [] args) {

        A a = new A("A");
        B b = new B("A", "B", "C");

        System.out.println("A: " + a.hashCode());
        System.out.println("B: " + b.hashCode());
    }

}

1

표준 구현은 약하며이를 사용하면 불필요한 충돌이 발생합니다. 상상해보십시오

class ListPair {
    List<Integer> first;
    List<Integer> second;

    ListPair(List<Integer> first, List<Integer> second) {
        this.first = first;
        this.second = second;
    }

    public int hashCode() {
        return Objects.hashCode(first, second);
    }

    ...
}

지금,

new ListPair(List.of(a), List.of(b, c))

new ListPair(List.of(b), List.of(a, c))

이를 가지고 hashCode즉, 31*(a+b) + c승산기가 사용 된List.hashCode 여기에 다시 사용됩니다. 분명히 충돌은 피할 수 없지만 불필요한 충돌을 일으키는 것은 단지 ... 불필요합니다.

를 사용하는 것에 대해 현명한 것은 없습니다 31. 승수는 정보 손실을 피하기 위해 홀수 여야합니다 (승수조차도 가장 중요한 비트를 잃고 4의 배수는 2를 잃는 등). 홀수 승수를 사용할 수 있습니다. 작은 승수는 더 빠른 계산으로 이어질 수 있지만 (JIT는 시프트 및 덧셈을 사용할 수 있음), 현대 인텔 / AMD에서 곱셈의 대기 시간이 3주기에 불과하다는 점은 중요하지 않습니다. 작은 승수는 또한 작은 입력에 대해 더 많은 충돌을 일으켜 때로는 문제가 될 수 있습니다.

소수는 링 Z / (2 ** 32)에서 의미가 없으므로 소수를 사용하는 것은 의미가 없습니다.

따라서 무작위로 선택한 큰 홀수를 사용하는 것이 좋습니다 (소중하게 생각하십시오). i86 / amd64 CPU는 단일 부호있는 바이트에 맞는 피연산자에 대해 더 짧은 명령어를 사용할 수 있으므로 109와 같은 곱셈기에는 약간의 속도 이점이 있습니다. 충돌을 최소화하려면 0x58a54cf5와 같은 것을 사용하십시오.

다른 장소에서 다른 승수를 사용하는 것이 도움이되지만 추가 작업을 정당화하기에는 충분하지 않을 수 있습니다.


0

해시 값을 결합 할 때 일반적으로 boost c ++ 라이브러리에서 사용되는 결합 방법을 사용합니다.

seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);

이것은 균등 한 분배를 보장하는 상당히 좋은 일입니다. 이 수식의 작동 방식에 대한 자세한 내용은 StackOverflow 게시물 : boost :: hash_combine의 매직 번호를 참조하십시오.

http://burtleburtle.net/bob/hash/doobs.html 에서 다양한 해시 함수에 대한 좋은 토론이 있습니다.


1
이것은 C ++이 아닌 Java에 대한 질문입니다.
dano

-1

간단한 클래스의 경우 equals () 구현으로 확인되는 클래스 필드를 기반으로 hashCode ()를 구현하는 것이 가장 쉬운 경우가 많습니다.

public class Zam {
    private String foo;
    private String bar;
    private String somethingElse;

    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (obj == null) {
            return false;
        }

        if (getClass() != obj.getClass()) {
            return false;
        }

        Zam otherObj = (Zam)obj;

        if ((getFoo() == null && otherObj.getFoo() == null) || (getFoo() != null && getFoo().equals(otherObj.getFoo()))) {
            if ((getBar() == null && otherObj. getBar() == null) || (getBar() != null && getBar().equals(otherObj. getBar()))) {
                return true;
            }
        }

        return false;
    }

    public int hashCode() {
        return (getFoo() + getBar()).hashCode();
    }

    public String getFoo() {
        return foo;
    }

    public String getBar() {
        return bar;
    }
}

가장 중요한 것은 hashCode ()와 equals ()의 일관성을 유지하는 것입니다. 두 객체에 대해 equals ()가 true를 반환하면 hashCode ()는 동일한 값을 반환해야합니다. equals ()가 false를 반환하면 hashCode ()는 다른 값을 반환해야합니다.


1
SquareCog처럼 이미 눈치 have습니다. 두 개의 문자열을 연결하여 해시 코드를 한 번 생성하면 대량의 충돌을 생성하는 것이 매우 쉽습니다 ("abc"+""=="ab"+"c"=="a"+"bc"==""+"abc"). 심각한 결함입니다. 두 필드의 해시 코드를 평가 한 다음 선형 조합을 계산하는 것이 좋습니다 (프라임을 계수로 사용하는 것이 좋습니다).
Krzysztof Jabłoński

@ KrzysztofJabłoński 맞아. 또한 스와핑 foobar불필요한 충돌도 발생합니다.
maaartinus
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.