[아이템 11] equals를 재정의하려거든 hashCode도 재정의하라

  • equals를 재정의한 클래스 모두에서 hashCode재정의해야함

  • 그렇지 않으면 해당 클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킴

  • hashCode 재정의를 잘못했을 때 크게 문제되는 사항은 논리적으로 같은 객체는 같은 해시코드를 반환해야 하는 부분임

  • equals에서는 물리적으로 다른 두 객체를 논리적으로 같다고 했지만 hashCode에서는 이 둘을 전혀 다르다고 판단하여 서로 다른 값을 반환함

  • hashCode를 재정의하지 않으면 논리적 동치인 두 객체가 서로 다른 해시코드를 반환하여 규약이 깨짐

  • 그리고 설령 같은 버킷에 담았어도 해시코드가 다른 엔트리끼리는 동치성 비교를 시도조차 하지 않도록 최적화되어 있어서 안 함

  • 아래와 같이 구현한다면 최악의 상황임

@Override public int hashCode() { return 42; }
  • 이는 모든 객체에게 똑같은 값만 내주어 모든 객체가 마치 연결리스트처럼 되버림, 객체가 많아지면 쓸 수 없게됨

  • 좋은 해시 함수라면 서로 다른 인스턴스에 다른 해시코드를 반환함

  • hashCode를 재정의한다면 아래와 같은 방식으로 씀

  1. int 변수 result를 선언한 후 값 c로 초기화함, 이때 c는 해당 객체의 첫번째 핵심 필드를 단계 2.a 방식으로 계산한 해시코드임(여기서 핵심 필드는 equals 비교에 사용되는 필드를 말함)

  2. 해당 객체의 나머지 핵심 필드 f각각에 대해 다음 작업을 수행함

    a. 해당 필드의 해시코드 c를 계산함

    i. 기본 타입 필드라면, Type.hashCode(f)를 수행함(여기서 Type은 해당 기본 타입의 박싱 클래스임)
     
     ii. 참조 타입 필드면서 이 클래스의 equals 메서드가 이 필드의 equals를 재귀적으로 호출해 비교한다면, 
     이 필드의 hashCode를 재귀적으로 호출함, 계산이 더 복잡해질 것 같으면, 이 필드의 표준형을 만들어 
     그 표준형의 hashCode를 호출함, 필드의 값이 null이면 0을 사용함
     
     iii. 필드가 배열이라면, 핵심 원소 각각을 별도 필드처럼 다룸, 이상의 규칙을 재귀적으로 적용해 각 핵심 원소의
     해시코드를 계산한 다음, 단계 2.b 방식으로 갱신함, 배열에 핵심 원소가 하나도 없다면 단순히 상수(0을 추천)를 사용함
     모든 원소가 핵심 원소라면 Arrays.hashCode를 사용함

    b. 단계 2.a에서 계산한 해시코드 cresult를 갱신함, 코드로는 다음과 같음
    result = 31 * result + c;

  3. result를 반환함

  • hashCode를 다 구현했다면 이 메서드가 동치인 인스턴스에 대해 똑같은 해시코드를 반환할지 자문해봄

  • 여기서 다른 필드로부터 계산해 낼 수 있는 필드는 모두 무시해도 됨, equals비교에 사용되지 않은 필드는 반드시 제외해야함

  • 여기서 위와 같이 hashCode를 작성할 수 있고 Object 클래스에서 임의의 개수만큼 객체를 받아 해시코드를 계산해주는 정적 메서드인 hash를 사용할 수 있음(단, 성능이 민감하지 않은 상황에서만 쓰는게 좋음)

  • 클래스가 불변이고 해시코드를 계산하는 비용이 크다면 매번 새로 계산하기보다는 캐싱하는 방식을 고려해야 하는데 이때 지연 초기화 전략을 통해서 응용할 수 있음(주로 해시의 키로 사용될 것 같다면, 스레드 안전성을 고려해야함)

  • 성능을 높인답시고 해시코드를 계산할 때 핵심필드를 생략하면 안됨

  • hashCode가 반환하는 값의 생성 규칙을 API 사용자에게 자세히 공표하지 말자, 그래야 클라이언트가 이 값에 의지하지 않고 추후에 계산 방식을 바꿀 수 있음

             

좋은 웹페이지 즐겨찾기