본문 바로가기

Java

equals()와 hashCode()에 대해

1. equals와 hashCode 란?


equals와 hashCode는 모든 Java 객체의 부모 객체인 Object 클래스에 정의되어 있다.

그러므로 Java의 모든 객체는 Object 클래스에 정의된 두 개의 메서드를 상속받고 있다.

 

[ equals() 란? ]

기본적으로 2개의 객체가 동일한지 검사하기 위해 사용 된다.

equals가 구현된 방법은 2개의 객체가 참조하는 것이 동일한지를 확인하는 것이며, 이는 동일성(identity)을 비교하는 것이다.

즉 2개의 객체가 가리키는 곳이 동일한 메모리 주소일 경우에만 동일한 객체가 된다.

public boolean equals(Object obj) {
    return (this == obj);
}
  • equals() Contract
    1. reflexive: an object must equal itself
    2. symmetric: x.equals(y) must return the same result as y.equals(x)
    3. transitive: if x.equals(y) and y.equals(z), then also x.equals(z)
    4. consistent: the value of equals() should change only if a property that is contained in equals() changes (no randomness allowed)

 

[ hashCode() 란? ]

실행 중에(Runtime) 객체의 유일한 integer값을 반환한다. (객체의 주소값이 아닌 주소값으로 만든 고유한 숫자값이다.)

Object 클래스에서는 heap에 저장된 객체의 메모리 주소를 반환하도록 되어있다.(항상 그런 것은 아님)

public native int hashCode();
  • hashCode() Contract
    1. internal consistency: the value of hashCode() may only change if a property that is in equals() changes
    2. equals consistency: objects that are equal to each other must return the same hashCode
    3. collisions: unequal objects may have the same hashCode

 

동일성: 두 객체의 주소값이 같으면 동일하다고 할 수 있다. primitive type의 경우 내용을 비교함 (== 연산자)
동등성: 두 객체가 같은 정보를 갖고 있는 경우. 참조하고 있는 객체의 주소가 서로 다르더라도 내용만 같으면 두 변수는 동등하다고 이야기 할 수 있다. (equals() 메서드)

 

2. equals와 hashCode 오버라이딩 및 주의점


보통 equals와 hashCode는 함께 오버라이딩 하여 사용하라고들 한다.

왜 그런 걸까?

결론부터 말하자면 두 메서드를 함께 오버라이딩 하지 않고, hash값을 사용하는 Collection Framework(e.g HashSet, HashMap, HashTable)를 사용할 때 의도치 않은 동작이 발생할 수 있기 때문이다.

 

아래의 예시를 확인해 보자.

class Money {
    int amount;
    String currencyCode;

    public Money(int amount, String currencyCode) {
        this.amount = amount;
        this.currencyCode = currencyCode;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return amount == money.amount && Objects.equals(currencyCode, money.currencyCode);
    }
/*
    @Override
    public int hashCode() {
        return Objects.hash(amount, currencyCode);
    }
*/
}
Money income = new Money(55, "USD");
Money expenses = new Money(55, "USD");

// equals를 오버라이딩 했으므로 동등함
boolean balanced = income.equals(expenses); // true

int i = income.hashCode();
int i1 = expenses.hashCode();
boolean b = i == i1; // false

HashSet<Money> moneys = new HashSet<>();
moneys.add(income);
moneys.add(expenses);

int size = moneys.size(); // 2

Money클래스에서 equals()를 오버라이딩 했으므로 balanced의 결과값은 true가 나올 것이다.

그리고 동등한 두 객체를 HashSet 자료구조 변수인 moneys에 넣었다.

Set 자료구조의 특성으로 인해 우리가 의도한 대로라면 moneys의 size는 1이 나오길 기대했을 것이다.

하지만 moneys의 size를 확인해 보면 2가 나온다.

income과 expenses는 논리적으로 같다고 정의하였지만 해시코드가 다르므로 두 개의 객체 모두 moneys에 추가된 것이다.

 

hashCode와 equals 동작 순서

위처럼 동작되는 이유는 hash값을 사용하는 Collection(HashMap, HashSet, HashTable)은 객체가 논리적으로 같은지 비교할 때 위 그림과 같은 과정을 거치기 때문이다.

 

가장 먼저 데이터가 추가되면, 그 데이터의 hashCode()의 리턴 값을 컬렉션에 가지고 있는지 비교한다.

만일 해시코드가 같다면 다음으로 equals()의 리턴 값을 비교하게 되고, true이면 논리적으로 같은 객체라고 판단한다.

 

위의 예제에서도 Money 클래스에는 hashCode()가 오버라이딩 되어있지 않아서 Object클래스의 hashCode()가 사용되었고, 

income, expenses 두 객체의 hashCode가 다르므로 equals로 비교도 하기 전에 다른 객체로 판단되서 moneys에 들어간 것이다.

따라서 이러한 예상치 못한 결과를 방지하기 위해 hashCode()도 오버라이딩 할 필요가 있다.

 

위의 주석처리된 오버라이딩된 hashCode()를 주석해제 후 실행하면 moneys의 size가 우리가 의도한 대로 1이 되는 것을 확인할 수 있다.

 

 

 

 

https://steady-coding.tistory.com/534

https://mangkyu.tistory.com/101

https://www.baeldung.com/java-equals-hashcode-contracts

'Java' 카테고리의 다른 글

정규표현식  (0) 2023.01.11
Java Garbage Collection  (0) 2022.09.22