equals, hashCode의 중요성2

2024. 6. 13. 15:08김영한 Java/컬렉션 프레임워크

hashCode는 구현했지만 equals를 구현하지 않은 경우

package collection.set.member;

import java.util.Objects;

public class MemberOnlyHash {
    private String id;

    public String getId() {
        return id;
    }

    public MemberOnlyHash(String id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}
  • Objects.hash(id)를 사용해서 id를 기준으로 해시 코드를 생성했다.
package collection.set.member;

import collection.set.MyHashSetV2;

public class HashAndEqualsMain2 {

    public static void main(String[] args) {
        //중복 등록
        MyHashSetV2 set = new MyHashSetV2(10);
        MemberOnlyHash m1 = new MemberOnlyHash("A");
        MemberOnlyHash m2 = new MemberOnlyHash("A");

        System.out.println("m1.hashCode() = " + m1.hashCode());
        System.out.println("m2.hashCode() = " + m2.hashCode());
        System.out.println("m1.equals(m2) = " + m1.equals(m2));

        set.add(m1);
        set.add(m2);
        System.out.println(set);

        //검색 실패
        MemberNoHashNoEq searchValue = new MemberNoHashNoEq("A");
        System.out.println("searchValue.hashCode = " + searchValue.hashCode());
        boolean contains = set.contains(searchValue);
        System.out.println("contains = " + contains);

    }
}

 

실행 결과

m1.hashCode() = 96
m2.hashCode() = 96
m1.equals(m2) = false
MyHashSetV2{buckets=[[], [], [], [], [], [], [collection.set.member.MemberOnlyHash@60, collection.set.member.MemberOnlyHash@60], [], [], []], size=2, capacity=10}
searchValue.hashCode = 1848402763
contains = false

 

데이터 저장 문제

  • hashCode()를 재정의했기 때문에 같은 id를 사용하는m1, m2는 같은 해시 코드를 사용한다.
  • 따라서 같은 해시 인덱스에 데이터가 저장된다.
  • 그런데 add() 로직은 중복 데이터를 체크하기 때문에 같은 데이터가 저장되면 안된다.
  public boolean add(Object value){
        int hashIndex = hashIndex(value);
        LinkedList<Object> bucket = buckets[hashIndex];
        if(bucket.contains(value)){
            return false;
        }
        bucket.add(value);
        size++;
        return true;
    }
  • bucket.contains() 내부에서 데이터를 순차 비교할 때 equals()를 사용한다.
  • 그런데 MemberOnlyHash는 equals()를 재정의하지 않았으므로 Object의 equals()를 상속 받아서 사용한다. 따라서 인스턴스의 참조값을 비교한다. 인스턴스가 서로 다른 m1,m2는 비교에 실패한다.
  • add() 로직은 중복 데이터가 없다고 생각하고 m1,m2를 모두 저장한다.
  • 결과적으로 같은 회원 id를 가진 중복 데이터가 저장된다.

 

데이터 검색 문제

  • MemberOnlyHash searchValue = new MemberOnlyHash("A")
  • 회원 id가 "A"인 객체를 검색하기 위해 회원 id가 "A"인 객체를 만들었다. 해시 코드가 구현되어 있다.
  • searchValue는 해시 인덱스 6을 정확히 찾을 수 있다.
  • 해시 인덱스에 있는 모든 데이터를 equals()를 통해 비교해서 같은 값을 찾아야 한다.
  • 다음 코드를 보자. bucket.contains(searchValue) 내부에서 연결 리스트에 있는 모든 항목을 searchValue와 equals()로 비교한다.
public boolean contains(Object searchValue){
        int hashIndex = hashIndex(searchValue);
        LinkedList<Object> bucket = buckets[hashIndex];
        return bucket.contains(searchValue);
    }
  • MemberOnlyHash는 equals()를 재정의하지 않았으므로 Object의 equals()를 상속 받아서 사용한다. 따라서 인스턴스의 참조값을 비교한다. 인스턴스가 서로 다른 searchValue와 m1, m2는 비교에 실패한다.
  • 결과적으로 데이터를 찾을 수 없다.

 

hashCode와 equals를 모두 구현하는 경우

package collection.set.member;

import collection.set.MyHashSetV2;

public class HashAndEqualsMain3 {

    public static void main(String[] args) {
        //중복 등록
        MyHashSetV2 set = new MyHashSetV2(10);
        Member m1 = new Member("A");
        Member m2 = new Member("A");

        System.out.println("m1.hashCode() = " + m1.hashCode());
        System.out.println("m2.hashCode() = " + m2.hashCode());
        System.out.println("m1.equals(m2) = " + m1.equals(m2));

        set.add(m1);
        set.add(m2);
        System.out.println(set);

        //검색 실패
        Member searchValue = new Member("A");
        System.out.println("searchValue.hashCode = " + searchValue.hashCode());
        boolean contains = set.contains(searchValue);
        System.out.println("contains = " + contains);

    }
}

 

실행 결과

m1.hashCode() = 96
m2.hashCode() = 96
m1.equals(m2) = true
MyHashSetV2{buckets=[[], [], [], [], [], [], [Member{id='A'}], [], [], []], size=1, capacity=10}
searchValue.hashCode = 96
contains = true

 

데이터 저장

  • 처음에 m1을 저장한다.
  • 다음으로 m2 저장을 시도한다. m1과 같은 해시 코드를 사용하므로 해시 인덱스도 같다.
  • 여기서 중복 데이터를 저장하면 안되므로 m1과 m2를 equals 비교한다. 같은 데이터가 이미 있으므로 m2는 저장에 실패한다.
  • 결과적으로 중복 데이터가 저장되지 않는다.

데이터 검색

  • searchValue의 해시 코드로 6번 해시 인덱스를 찾는다.
  • searchValye와 6번 해시 인덱스 내부의 데이터를 모두 equals 비교한다. searchValue와 m1이 equals비교에 성공하므로 참을 반환한다.

정리

hashCode()를 항상 재정의해야 하는 것은 아니다. 하지만 해시 자료 구조를 사용하는 경우 hashCode()와 equals()를 반드시 함께 재정의해야 한다. 물론 직접 재정의하는 것은 쉽지 않으므로 IDE의 도움을 받자.