equals 메소드와 hashCode 메소드 재정의는 언제해야할까?

equals 메소드

우리는 흔히 equals 는 값비교, == 는 주소값 비교라고 한다. 왜 equals는 값비교일까?

 

기본적으로 자바의 모든 클래스들은 Object 클래스를 상속하게 된다. 즉, 모든 클래스는 Object 클래스의 equals를 가지게 된다. Object 클래스의 equals 메소드는 오로지 자기 자신만이 true를 낼 수 있는 결과를 만들것이다. 

 

String 클래스로 예를 들어보자.
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

- JDK 1.8 기준 String 클래스의 equals 메소드이다. 로직을 살펴보면 value 라는 char[] 배열에 String의 문자열의 문자들이 모두 저장되어있다. 이를 하나 하나 꺼내서 Parameter로 넘어온 anObject 인스턴스를 String으로 타입변환하여 비교하는 형태이다.

 

이번에는 Integer 클래스를 예로 들어보자.
public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

- JDK 1.8 기준 Integer 클래스의 equals 메소드이다. Parameter obj 객체를 Integer 타입으로 형변환한 후 intValue() 메소드를 호출하여 주소값 비교 형태로 계산한다. intValue() 메소드는 int 타입의 원시형 값을 반환하므로 결국 원시타입 비교로 끝을 낸다.

 

기본적으로 Java에서 자주 사용되는 문자열과 숫자 타입의 참조타입 객체인 String 과 Integer 클래스의 equals 메소드 구현내용을 살펴보았다. 어쨌든 각자의 클래스 타입에 맞는 equals 메소드가 재정의된 것을 확인할 수 있었다.

 

그렇다면 우리가 프로그래밍을 할때 생성되는 일반 객체들은 과연 equals 메소드를 어떻게 재정의해주어야 할까?? 꼭 재정의 해줘야 되는 건가??

class Product {
    private int a;
    private String b;

    public Product(int a, String b) {
        this.a = a;
        this.b = b;
    }

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    public String getB() {
        return b;
    }

    public void setB(String b) {
        this.b = b;
    }
}

위와 같은 VO(value Object) 객체는 어떻게 재정의를 해줘야할까?

 

일단은 재정의 필요성 여부부터 판단해야한다. "Product 객체의 인스턴스 참조를 통해 equals 메소드를 호출하여 객체의 값을 비교할 부분이 있는가??" 정도부터 시작하면 될것같다.!!

 

필요하다 판단이 섰다면, 일단 Product 클래스가 Object 클래스에 정의된 equals 메소드를 따라야 하는가부터 판단해야한다. Object 클래스의 equals 메소드는 반드시 자기자신만 true 값을 내도록 주소값 비교를 진행하고 있다.

 

하지만 Product 객체는 단지 a, b 변수의 값만 같으면 같은 객체로 인식시키도록 구현해야한다고 치자!

 

그렇다면 재정의를 해주어야한다.

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Product)) {
            return false;
        }
        Product product = (Product) obj;
        return product.getA() == this.a && Objects.equals(product.getB(), this.b);
    }

위에 처럼 재정의를 해주었다.

같은 값을 가지는(a 와 b 변수가 같은 값을 가지는) Product 인스턴스끼리는 이제 equals 메소드 호출 시 true의 값을 던지게 될 것이다.

 

이렇게 하면 무난하게 어느 상황에서든지 객체의 값을 비교하고 Product 객체가 같은값을 가지는지 비교할 수 있을 것이다.

 

그런데 equals 메소드를 재정의하는 순간 반드시 기억해야할 파트너가 있다.

 

 


 

hashCode 메소드

자, 지금부터 알아두자. equals 메소드를 재정의한 순간 이 메소드도 함께 재정의되어야 한다. 이건 반드시다. equals 메소드를 재정의 하든지 말든지는 해당 객체를 어떻게 사용하느냐에 따라 달라지겠지만. 일단 equals를 재정의 했다면 반드시 hashCode 메소드는 재정의되어야한다.

 

HashMap을 살펴보자

 

갑자기 왠 HashMap?? 이라고 할테지만 공통점이 있다. Hash라는 단어..! 즉, HashMap은 내부에서 hash를 하여 데이터를 저장하는 Map 구조이다.

 

Map은 기본적으로 Key와 Value의 쌍으로 데이터를 관리하며 Key는 Value를 획득하기 위한 열쇠로 사용된다.

다음의 코드를 보자. put() 메소드를 호출하면 안속에서 어떤 동작을 수행할까??

 

HashMap<String, Integer> map = new HashMap<>();
map.put("해쉬", 1);

 

put 메소드 내부

HashMap 객체의 put 메소드
HashMap 객체의 hash 메소드

put 메소드를 호출하는 순간 HashMap에서는 "해쉬"라는 String 객체의 값을 hash 하여 String 클래스의 hashCode 메소드를 호출하게 된다. 즉, key의 값은 hashCode 메소드에 의해서 생성된 해시값을 토대로 관리되며 hashCode의 값이 제대로 정의되지 않는다면(혹은 equals를 재정의하고도 hashCode를 재정의하지 않는다면...) 아래의 get 메소드로 key를 꺼낼 때 과연 제대로된 키 역할을 할 수 있을까?? 하는 의심이 들게 될 것이다.

HashMap 객체의 get 메소드 내부

get 메소드를 보면 getNode의 인자로 hash(key) 가 넘어가게 된다. 즉, key로 넘어온 파라미터를 hash하여 해당 Node를 찾는다. 즉 key의 hashcode를 활용하여 찾게된다.

 

Product p1 = new Product(1, "1");
Product p2 = new Product(1, "1");

System.out.println(p1.hashCode() == p2.hashCode());

hashCode 메소드를 재정의하지 않은 시점인 지금 print 문의 결과는 false 일 수밖에 없다. Object 클래스의 hashCode 메소드는 native 메소드로 정의되어 있기 때문에 내부를 정확히 들여다 볼 수는 없지만, 자기 자신외에는 전부 false를 내보내는 equals 메소드와 마찬가지로 자기 자신을 제외한 모든 인스턴스는 다른 hashCode를 갖게 된다.(Object 클래스의 hashCode 메소드는 같은 타입이라도 다른 인스턴스라면 hashCode는 다른 값을 가진다.)

 


결국 hashCode를 재정의하지 않을 경우 HashMap이나 HashTable과 같은 key value 구조의 연관배열의 경우 key를 제대로 찾지 못하게 될 수 있다. 즉, Product의 hashCode 메소드를 하지 않게되고 HashMap, HashTable의 key로 Product 타입을 사용하게 된다면 Object 클래스의 hashCode를 활용하게 될 것이고, 자기자신외에는 모두 다른 hashCode 값을 생성할 것이므로 제대로된 key, value 구조를 활용할 수 없게된다.

 

@Override
public int hashCode() {
    return Objects.hash(a, b);
}

위의 hashCode 메소드를 재정의해주면 a와 b 멤버변수의 값을 토대로 hash 값을 생성하므로 a, b의 값이 같게된다면 같은 hashCode의 결과를 갖게 된다.

 

따라서 HashMap에서의 Key로 Product 타입을 활용할 수 있게 된다.

 


결론

1. equals()를 재정의하였다면 반드시 hashCode()를 재정의할 것.

2. VO 객체에는 equals()와 hashCode()를 재정의할 것.

 

댓글

Designed by JB FACTORY