equals를 재정의하려거든 hashCode도 재정의 하라(Effective Java).

Choizz·2023년 8월 8일
0

이펙티브 자바

목록 보기
10/13

이번 포스팅은 이펙티브 자바의 아이템 중 "equals를 재정의하려거든 hashcode도 재정의 하라."에 대한 내용입니다.

개발을 할때 equals와 hashcode를 같이 재정의해야 한다는 말을 많이 들어봤을 것입니다.
그 이유에 대해서 알아 보겠습니다.


hashCode 규약

  • equals 비교에 사용하는 정보가 변경되지 않았다면 hashcode는 매번 같은 값을 리턴해야 합니다.
  • 어떤 객체를 가지고 올 때, 변경이 없다면 같은 hashcode가 매번 리턴이 돼야합니다.
  • 따라서, 두 객체에 대한 equals가 같다면, hashcode의 값도 같아야 합니다.
  • 두 객체에 대한 equals가 다르더라도, hashCode의 값은 같을 수 있지만 해시 테이블의 성능을 고려해 다른 값을 리턴하는 것이 좋습니다.

잠깐 해시 충돌에 대해서 다루어 보겠습니다.

해시 충돌

  • equals가 다르더라도, hashCode의 값은 같은 것을 해시 충돌이라고 합니다.
  • 보통 해시 충돌을 해결하기 위해 Seperate chainingOpen addressing 기법을 사용합니다.

Seperate Chaining 기법

  • 개방 해싱 기법 중 하나로, 해시 테이블 저장공간 외의 저장공간을 활용합니다.
  • 충돌 시, 링크드 리스트, 트리(Java 8)를 활용하여 데이터를 추가로 뒤에 연결시켜 저장합니다.

Open Addressing 기법

  • 폐쇄 해싱 기법 중 하나로, 해시 테이블 저장 공간 안에서 충돌 문제를 해결합니다.
  • 충돌 시 해당 해시 주소 다음 주소부터 맨 처음 나오는 빈공간에 저장을 합니다.
    • 둘 다 시간 복잡도는 O(n)이지만, open addressing은 연속된 공간에 데이터를 저장하기 때문에 seperate chaining에 비해 캐시 효율이 좋습니다.
    • 데이터가 적다면 open addressing이 효율이 좋습니다.

Java의 HashMap Seperate Chaning을 사용하는 이유

  • Open Addressing 기법은 데이터를 삭제할 때, 처리가 효율적이지 않지만, HashMap에서는 remove() 메서드가 빈번하게 호출되기 때문입니다.

    • key를 삭제후, 삭제된 인덱스 부터 순차적으로 테이블을 돌면서 , 삭제된 데이터의 존재로 인해 다른 위체에 저장된 다른 값들을 찾고, 해당 값들을 알맞은 위치로 당겨줘야 합니다.
    • 해시 충돌 주소 다음 주소부터 저장을 하기 때문입니다.
  • HashMap에 저장된 키-값 쌍 개수가 일정 개수 이상으로 많아지면, 일반적으로 Open Addressing은 Separate Chaining보다 느립니다.

    • Open Addressing의 경우 해시 버킷을 채운 밀도가 높아질수록 Worst Case 발생 빈도가 더 높아집니다.
    • separate Chaining 방식의 경우 보조 해시 함수를 활용하여 해시 충돌이 잘 발생하지 않도록 '조정'할 수 있다면 Worst Case 또는 Worst Case에 가까운 일이 발생하는 것을 줄일 수 있습니다.

이러한 이유로, 해시 테이블 성능을 고려한다면 다른 값을 리턴하는 것이 좋습니다.


hashcode 구현 방법

  1. 핵심 필드 하나의 값의 해시 값을 계산해서 result(변수) 값을 초기화한다.
  2. result에 31 * 해당 필드의 hashcode 값을 계산한다.
    • 기본 타입은 Type.hashCode()
    • 참조 타입은 해당 필드의 hashCode()
    • 배열은 Arrays.hashCode()
  3. result를 리턴한다.
  • 보통 IDE나 lombok을 사용하여 구하거나, 구아바 등의 라이브러리, Object의 hash 메서드를 사용합니다.
  • 불변 클래스라면 해싱을 통해 해시코드 계산 비용을 줄일 수 있습니다.

주의 사항

  • 스레드 안정성을 고려해야 합니다.
    • 여러 스레드가 해시코드를 계산하게 되면, 같은 객체라도 다른 해시코드를 반환할 수 있습니다.
  • 핵심 필드를 hascode 계산할 때 빼면 안됩니다.
    • equals에서 쓰고 있습니다.
  • hashcode 계산 규칙을 api에 노출하지 않는 것이 좋습니다.
    • 개발자는 hashcode 규약을 따르도록 하는 것이 좋습니다.

equals와 hashCode를 같이 재정의해야하는 이유

결론을 먼저 말씀 드리면, Collection을 사용할 때, 예를 들어 hashSet에 저장을 한다고 할 때, 같은 객체임에도 불구하고, 다른 2개의 객체로 인식되어 저장될 수 있습니다.

public class Person{
	private String name;
    private int age;
    
    //생성자, getter등 생략..
}

아래의 코드는 당연히 false가 나옵니다. equals를 재정의하지 않았기 때문입니다.

Person p1 = new Person("name1", 10);
Person p2 = new Person("name1", 10);

p1.equals(p2) //false

equals를 재정의 해주면, p1과 p2는 같은 객체로 인식될 것입니다.

public class Person{
	private String name;
    private int age;
    
    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }
}
  • 하지만, Set에 두개의 객체를 넣는 다면 둘을 다른 객체로 인식합니다.
  • 자료구조는 자료를 저장하기 위한 위치를 선택하기 위해 hasgCode를 사용하는데, 해시코드가 재정의되지 않았기 때문에 다른 해시코드가 리턴되는 것 입니다.
   Person p1 = new Person("name", 10);
   Person p2 = new Person("name", 10);
   
   Set<Person> personSet = new HashSet<>();
   personSet.add(p1);
   personSet.add(p2);

   personSet.size() == 2 //true
  • hashCode를 재정의하게 되면 자료구조에서 같은 객체로 인식하고 하나의 객체를 저장할 것입니다.
public class Person{
	private String name;
    private int age;
    
    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
   Person p1 = new Person("name", 10);
   Person p2 = new Person("name", 10);
   
   Set<Person> personSet = new HashSet<>();
   personSet.add(p1);
   personSet.add(p2);

   personSet.size() == 1 //true

정리

  • equals와 hashCode는 재정의를 같이 해야합니다.
  • lombok과 IDE의 도움을 받을 수 있습니다.

reference

profile
집중

0개의 댓글