제육's 휘발성 코딩
article thumbnail
반응형

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

equals 재정의 후 hashCode를 재정의해야 한다. 그 이유는 논리적으로 같은 객체는 같은 해시코드를 반환해야하기 때문이다.

Hash 함수를 사용하는 컬렉션을 사용하지 않으면 hashCode를 재정의하지 않더라도 문제가 발생하지 않는다. 하지만 애플리케이션 레벨에서 바라봤을 때 해시 컬렉션을 사용하지 않는다고 확신하기 어렵다. 따라서 hashCode도 같이 재정의 하는 것이 필요하다.

 

equals만 재정의 한 경우

import java.util.*;

public class Car {
  private final String name;

  public Car(String name) {
    this.name = name;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Car car = (Car) o;
    return Objects.equals(name, car.name);
  }
}

class CarTest {
  public static void main(String[] args) {
    Car carA = new Car("sasca");
    Car carB = new Car("sasca");
    System.out.println(carA.equals(carB)); // true


    List<Car> carList = new ArrayList<>();
    carList.add(new Car("sasca"));
    carList.add(new Car("sasca"));
    System.out.println(carList.size()); // 2

    Set<Car> carSet = new HashSet<>();
    carSet.add(new Car("sasca"));
    carSet.add(new Car("sasca"));
    System.out.println(carSet.size()); //2 (문제 발생)
  }
}
  • equals만 재정의한 경우 두 객체는 논리적으로 같은 객체로 판단되며 equals는 true의 결과 값을 갖게 된다. 당연히 List 타입의 컬렉션에 두 객체를 넣으면 정상 동작한다.
  • Set 타입의 중복되지 않는 객체를 넣을 때 문제가 발생한다. 1이 나와야하지만, 예상과 다르게 2가 나온다. 그 이유는 hash 값을 사용하는 컬렉션을 사용할 때 문제가 발생하기 때문이다.

 

hash Collection 동작 과정

image

  • 컬렉션의 HashMap, HashSet, HashTable은 객체가 논리적으로 같은지 비교할 때 다음과 같은 과정을 거친다.
System.out.println(carA.hashCode()); // 2109957412
System.out.println(carB.hashCode()); // 901506536
  • 두 객체의 Object 클래스의 hashCode를 출력해보면 다르다는 것을 볼 수 있다. 즉, hashCode가 다르기 때문에 equals 비교를 하기 전에 다른 객체로 판단이 된다.

 

hashCode도 같이 재정의한 경우

@Override
public int hashCode() {
  // return Objects.hash(name); JAVA 7
  return name.hashCode();
}

System.out.println(carA.hashCode()); // 109208323
System.out.println(carB.hashCode()); // 109208323

Set<Car> carSet = new HashSet<>();
carSet.add(new Car("sasca"));
carSet.add(new Car("sasca"));
System.out.println(carSet.size()); // 1 정상 동작
  • hashCode를 재정의 해줌으로 같은 해시 코드 값을 반환하게 되었다. 따라서 해시 컬렉션에서 equals를 비교할 수 있게 되고 동등 객체로 판단이 된다.
  • 자바 7버전 부터 사용 가능한 Objects.hash() 유틸리티 메서드, 롬복의 @EqualsAndHashCode를 통해 편리한 해싱을 적용할 수 있다.

 

HashCode 구현 과정

@Override
public int hashCode(){
  return 31;
}
  • 사실 다음과 같이 별도의 연산 과정 없이 정수를 리턴해도 재정의를 통해 보장할 수 있다. 하지만 동등 객체가 아닌 다른 객체에서도 같은 값을 반환하기 때문에 해시 테이블의 버킷이 연결리스트로 존재하여 성능이 O(1)인 해시테이블이 O(N)이 된다.
@Override
public int hashCode(){
  return Objects.hash(name);
}
  • 해당 방식은 위에서 설명한 자바 7에서 제공하는 유틸리티 메서드를 이용하는 방식이다. 정수를 리턴하는 것보다 효율적이지만 매번 계산하는 방식의 성능이 아쉽다.
@Override
public int hashCode() {
  int result = hashCode;
  if (result == 0) {
    result = 31 * name.hashCode();
    // result = 31 * result + Short.hashCode(prefix);
    hashCode = result;
  }
  return result;
}
  • 지연 초기화 전략을 통해 캐싱하는 방식을 고려하면 최적의 성능을 보여줄 수 있을 것이다. 단, 필드를 지연 초기화하려면 스레드 세이프하도록 신경써야 한다.
  • hashCode 를 구현하는 것에 보편적인 방법은 없지만 대부분 31번을 활용한다. 그 이유는 31이 소수이면 어떤 수에 31을 곱하는 것을 shift 연산자로 빠르게 계산할 수 있기 때문이다. 예를 들어 31N = 32N - N 에서 2^5은 32니까 31N = (N<<5) - N 으로 표현할 수 있다.

REFERENCES

https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/

https://www.baeldung.com/java-hashcode

https://d2.naver.com/helloworld/831311

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^