equals, hashCode의 중요성 1

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

해시 자료 구조를 사용하려면 hashCode()도 중요하지만, 해시 인덱스가 충돌할 경우를 대비해서 equals()도 반드시 재정의해야 한다. 해시 인덱스가 충돌할 경우 같은 해시 인덱스에 있는 데이터들을 하나하나 비교해서 찾아야한다. 이때 equals()를 사용해서 비교한다.

 

참고

해시 인덱스가 같아도 실제 저장된 데이터는 다를 수 있다. 따라서 특정 인덱스에 데이터가 하나만 있어도 equals()로 찾는 데이터가 맞는지 검증해야 한다.

앞의 예에서 "hi"라는 회원과 "JPA"라는 회원의 해시 인덱스는 둘다 0으로 같다.

만약 "hi"라는 회원만 저장했다고 가정하자, 이렇게 되면 0번 인덱스에는 하나의 데이터만 보관되어 있다. 이때 "JPA"라는 회원을 찾는다면 같은 0번 인덱스에서 찾는다. 이때 equals()를 사용해서 "JPA"라는 회원이 맞는지 검증해야 한다. 0번 인덱스에는 "hi"라는 회원만 있으므로 데이터를 찾는데 실패한다.

 

따라서 해시 자료 구조를 사용하려면 반드시 hashCode()와 equlas()를 구현해야 한다.

지금부터 hashCode(), equals()를 제대로 구현하지 안흥면 어떤 문제가 발생하는지 알아보자.

참고조 자바가 제공하는 String, Integer같은 기본 클래스들은 대부분 hashCode(), equals()가 재정의되어 있다.

 

Object의 기본 기능

  • hashCode() : 객체의 참조값을 기반으로 해시 코드를 반환한다.
  • equals() : == 동일성 비교를 한다. 따라서 객체의 참조값이 같아야 true를 반환한다.

클래스를 만들 때 hashCode(), equals() 를 재정의하지 않으면, 해시 자료 구조에서 Object가 기본으로 제공하는 hashCode(), equals()를 사용하게 된다. 그런데 Object가 기본으로 제공하는 기능은 단순히 인스턴스의 참조를 기반으로 작동한다.

 

지금부터 hashCode, equals를 제대로 구현하지 않은 경우 해시 자료 구조를 사용할 때 어떤 문제들이 발생하는지 하나씩 알아보자.

  • hashCode, equals를 모두 구현하지 않은 경우
  • hashCOde는 구현했지만 equals를 구현하지 않은 경우
  • hashCode와 equals를 모두 구현한 경우
package collection.set.member;

import collection.set.MyHashSetV2;

public class HashAndEqualsMain {
    public static void main(String[] args) {
        //중복 등록
        MyHashSetV2 set = new MyHashSetV2(10);
        MemberNoHashNoEq m1 = new MemberNoHashNoEq("A");
        MemberNoHashNoEq m2 = new MemberNoHashNoEq("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);

    }
}
  • hashCode, equals를 재정의 하지 않아 Object의 기본 기능을 사용한다.

 

실행 결과

m1.hashCode() = 1922154895
m2.hashCode() = 960604060
m1.equals(m2) = false
MyHashSetV2{buckets=[[MemberNoHashNoEq{id='A'}], [], [], [], [], [MemberNoHashNoEq{id='A'}], [], [], [], []], size=2, capacity=10}
searchValue.hashCode = 1996181658
contains = false

 

 

m1.hashCode, m2.hashCode 는 Object의 기본 기능을 사용하기 때문에 객체의 참조값을 기반으로 해시 코드를 생성한다. 따라서 실행할 때 마다 값이 달라질 수 있다. 여기서는 m1=1004, m2=1007이라고 가정하겠다.

m1과 m2는 인스턴스는 다르지만 둘다 "A"라는 같은 회원 id를 가지고 있다. 따라서 논리적으로 같은 회원으로 보아야 한다.

 

데이터 저장 문제

 

  • m1과 m2의 해시 코드가 서로 다르기 때문에 각각 다른 위치에 저장된다.
  • 회원 id가 "A"로 같은 회원의 데이터가 중복 저장된다.

 

데이터 검색 문제

  • MemberNoHashEq searchValue = new MemberNoHashNoEq("A")
  • 회원 id가 "A"인 객체를 검색하기 위해 회원 id가 "A"인 객체를 만들었다. 이 객체의 참조값은 1008이라 가정하자.
  • 데이터를 검색할 때 searchValue 객체의 해시코드는 1008이다. 따라서 다른 위치에서 데이터를 찾게 되고, 검색에 실패한다.