개발/Java

equals()와 hashCode()

Debin 2023. 12. 13.
반응형

자바에서 모든 클래스는 Object 클래스를 부모 클래스로 가진다.

Object 클래스에는 equals()와 hashCode()가 있다.

 

오늘은 이 둘에 대해 정리해보려고 한다.

 

동등성과 동일성

동일성은 두 객체가 동일한 인스턴스임을 의미하며 == 연산자로 확인이 가능하다.

동등이란 두 객체가 동일한 상태나 값을 갖는다는 것을 의미하며, equals 메서드로 확인할 수 있다.

 

동일성은 동등하다고 말할 수 있지만, 동등하다고해서 동일성을 가진다고 말할 수는 없다.

 

equals()

public boolean equals(Object obj) {
    return (this == obj);
}

 

equals()는 객체가 동등한지(논리적 동치성, 동등성)를 검사한다.

 

기본적으로 Object 클래스의 equals 메서드는 동일성을 검사한다. == 연산자가 그 증거다.

즉 우리가 작성한 클래스가 equals 메서드를 오버라이딩하지 않는다면 동일한 인스턴스가 아닌 이상 equals는 항상 false를 리턴한다.

 

그러나 우리는 equals 메서드를 통해 확인하고 싶은 것은 동일성이 아니라 동등성이다.

따라서 equals 메서드는 오버라이딩해주는 것이 적절하다.

 

equals 메서드 구현 단계

  1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인(성능 최적화 용)
  2. instanceof 연산자로 입력이 올바른 타입인지 확인한다. 그렇지 않다면 false를 리턴.
  3. 입력을 올바른 타입으로 형변환한다. instanceof를 사용하므로 무조건 성공
  4. 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 검사

 

hashCode()

public native int hashCode();

 

먼저 Object 클래스의 hashCode()는 객체에 대한 해시 코드 값을 반환한다.

 

해시 코드는 기본적으로 힙에 저장된 객체의 메모리 주소를 리턴한다.

해시 코드 메서드는 객체의 메모리 주소를 어떤 시점에 대한 메서드로 구현될 수도 있고 아닐 수도 있다.

 

해당 메서드는 해시 테이블과 같은 구조를 이용하는데 있어서 도움이 된다.

 

equals()와 hashCode()

equals를 오버라이딩한 모든 클래스는 hashCode도 반드시 오버라이딩해야 한다고 한다.

 

그렇지 않으면 hashCode 일반 규약을 어기게 되어 해당  클래스의 인스턴스를 HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킨다.

 

다음은 Object 명세에서 발췌한 규약이다.

  • equals 비교에 사용되는 정보가 변경되지 않았다면, 애플리케이션이 실행되는 동안 그 객체의 hashCode 메서드는 몇 번을 호출해도 일과되게 항상 같은 값을 반환해야 한다. 단 애플리케이션을 다시 실행한다면 이 값이 달라져도 상관없다.
  • equals(Object)가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 똑같은 값을 반환해야 한다.
  • equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 높아진다.

 

중요한 부분은 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다는 것이다.

 

equals를 오버라이딩 했는데 hashCode를 오버라이딩하지 않으면 HashMap, HashSet 같은 컬렉션의 원소로 사용할 때 문제가 발생한다고 앞에서 언급했다.

 

아래는 HashMap의 get() 메서드다.

get() 메서드는 동등성을 비교조차 하지 않는다. 

사용하는 equals() 메서드도 부모인 Object의 equals()를 그대로 사용한다.

 

즉 오로지 동일성만을 체크한다. 

 

 

HashMap의 get() 메서드

 

즉 프로그램에서 '값이 같으면 논리적으로 같은 인스턴스' 라는 생각을 가지고 HashMap을 사용하려면 반드시 hashCode를 정의해줘야 한다.

 

hashCode를 오버라이딩하지 않으면 동일하지 않는 이상(힙 주소가 같다) 다른 정수 값이 리턴되기 때문이다.

실습

import java.util.HashMap;
import java.util.Map;

public class App {

    static class Key {

        private String key;

        public Key(String key) {
            this.key = key;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Key)) return false;

            Key key1 = (Key) o;

            return key != null ? key.equals(key1.key) : key1.key == null;
        }

        @Override
        public int hashCode() {
            return key != null ? key.hashCode() : 0;
        }
    }

    public static void main(String[] args) {
        Map<Key, Integer> map = new HashMap<>();

        map.put(new Key("k"), 1);
        Integer result = map.get(new Key("k"));
        System.out.println("result = " + result); //hashCode 주석하면 null, 주석풀면 1

        Key k1 = new Key("k");
        Key k2 = new Key("k");

        System.out.println(k1.equals(k2)); //equals만 정의하면 true
    }
}

번외: hashCode를 잘못 오버라이딩하면 HashMap과 같은 콜렉션의 성능이 떨어진다?

Map은 Key, Value 구조다.

하나의 Key, Value는 Entry라는 클래스로 묶여있다.

 

Entry 구현 클래스 코드를 뜯어보자.

 

HashMap내부의 Entry 구현체

 

아래와 같이 Entry 클래스이면서 next라는 필드가 보인다.

여기서 눈치챌 수 있는 점은 Entry가 LinkedList처럼 동작한다는 것이다.

 

즉 해시 충돌을 체이닝으로 해결한 것이다.

 

여기서 문제가 발생할 수도 있는데 hashCode를 동일한 값을 자주 내주면 많은 객체가 하나의 해시테이블 버킷에 담긴다.

그러면 성능이 O(1)에서 O(n)으로 느려진다.

 

따라서 hashCode를 잘 오버라이딩해주어야 한다.

 

참고 자료

이펙티브 자바

https://www.linkedin.com/advice/0/how-do-you-implement-equals-hashcode-methods

 

반응형

댓글