Java

equals와 hashCode

뽀루피 2024. 10. 25. 18:46

내가 만든 객체로 List를 만들었는데 List 내에 중복되는 객체가 있다면 제거하고 싶다고 가정해보자.

List<Person> peopleList = new ArrayList<>();
people.add(new Person("loopy"))
people.add(new Person("loopy"))

Set<Person> peopleSet = new HashSet<>(people);

if(peopleList.size() != peopleSet.size()) {
    throw new IllegalArgumentException();
}

 

이 코드를 진행했는데 Exception이 발생하지 않고 다음 코드로 넘어간다. 왜일까?

정답은 내가 만든 객체에서 equals와 hashCode 메소드를 재정의(override)하지 않았기 때문이다.

 

 

equals와 hashCode란?

equals와 hashCode는 동일성보다는 동등성을 비교할 수 있도록 돕는 메소드이다. 동일성은 같은 메모리 공간을 가리키는 쉽게 얘기해 '=='이 성립하는 것이고, 동등성은 같은 메모리 공간을 가리키지 않아도 같은 값을 가지면 성립하는 것이다.

 

equals는 보통 String의 값을 비교할 때 많이 써보곤 한다. String은 참조 자료형으로 값을 비교할 때 동등성을 비교한다.

String str1 = "hello";
String str2 = "hello";

이 둘은 값은 같지만 메모리 공간이 다르다. 

str1 == str2  // false
str1.equals(str2)  // true

동일성 비교는 false지만 동등성 비교는 true인 모습

 

 

여기서 궁금증이 생긴다. 위에서 내가 만든 Person 객체 비교는 불가했는데 왜 String은 되는가?(참고로 List를 HashSet으로 전환할 때는 equals와 hashCode로 동등성을 비교한다)

 

모든 객체의 최상위 객체는 Object이다. 그리고 이 Object의 메소드 중에 equals와 hashCode가 존재한다. 그래서 모든 객체는 equals와 hashCode 메소드를 사용할 수 있는 것이다.

자바에서 제공하는 String, Integer, Double과 같은 객체들에는 이미 equals와 hashCode가 동등성을 비교할 수 있도록 재정의되어있다. 그래서 재정의를 추가로 할 필요가 없는 것이다. 하지만 우리가 만든 객체는 재정의가 안되어 있다. 우리가 손수 만들어줘야 한다.

@Override
public boolean equals(Object obj) {
    if(this == obj) return true;
    if(obj == null || getClass() != obj.getClass()) return false;
    Person person = (Person) obj;
    return name.equals(person.name);
}

먼저 동일성 비교를 하고 동등성 비교를 한다.

 

 

많은 경우 equals로만 값 비교를 하는 것 같은데 그럼 hashCode는 뭘까? 

hashCode는 해시 기반 자료구조의 동등성 비교에 필요하다. 해시 기반 자료구조는 데이터를 버킷이라는 묶음으로 나누어 저장하는데 hashCode를 통해 객체가 어느 버킷에 들어갈지 결정하고 다른 버킷에 들어있다면(hashCode가 다르다면) equals 검사를 생략하고 동등하지 않다고 판단하게 된다.

버킷은 이처럼 LinkedList 형태로 객체를 추가한다.

 

 

 

Object의 hashCode는 객체의 메모리 주소를 바탕으로 해시코드를 생성한다. 따라서 기본 hashCode는 두 객체가 같은 값을 가지더라도 메모리 주소가 다르면 서로 다른 해시 코드를 가진다.

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

 

그래서 hashCode를 재정의할 때 속성 값을 바탕으로 해시코드를 생성할 수 있도록 만든다. 이렇게 되면 속성 값이 같아 같은 버킷에 담길 수 있게 된다.

 

만약 hashCode는 같은데(같은 버킷에 저장) equals가 다르다면 어떻게 될까? 이 경우 다른 객체로 간주되며 해시 충돌을 일으키고 같은 버킷에 독립적인 객체로 존재하게 된다. 첫번째 예시처럼 Set 자료구조에 중복처리 없이 각각 존재하게 된다는 것이다.

 

 

해시 기반 자료구조를 사용하지 않는 경우에는 hashCode는 필요없나?

그렇다. 하지만 이후의 확장성이나 디버깅 시 객체의 주소를(기본 toString의 경우 hashCode가 포함되어 있음) 확인할 때를 고려한다면 함께 재정의해주는 편이 좋다고 생각한다.