[JAVA] equals()와 hashCode()의 관계

용용학생·2024년 8월 11일

자바

목록 보기
24/32
post-thumbnail

이번 글에서는 두 메서드에 대해서 왜 오버라이딩해야 하는지
그리고 어떻게 해야 올바르게 오버라이딩을 하는 건지 에 대하여 알아보고자 한다.

equals()hashCode()는 최상위 클래스인 java.lang.Object 에 선언된 메서드이다.

그렇기에 모든 자바 클래스는 암묵적으로 두 메서드를 사용할 수 있다.

equals 메서드

equals 메서드는 동일성이 아닌 동등성의 비교가 필요할 때 사용한다.

동일성 vs 동등성

  • 동일성 => 완전히 동일한 객체를 가리킴
  • 동등성 => 같은 정보를 가지고 있음

Object 클래스에 기본적으로 구현된 메서드를 사용한 예시를 보자.

class Money {
    int amount;
    String currencyCode;
}
Money income = new Money(50, "USD");
Money expenses = new Money(50, "USD");
boolean balanced = income.equals(expenses);

우리는 incomeexpenses가 같은 amount, currencyCode를 가졌기에
income.equals(expenses) 의 결과를 true로 기대한다.

그러나 예상과는 다르게 false가 반환된다.

그 이유는 Object 클래스의 equals는 이렇게 구현되어 있기 때문이다.

보이는 것처럼 기본 구현은 객체 동일성 비교 연산자인 ==로 되어있다.

언제 오버라이딩이 필요할까?

위와 같이 동등성 비교를 지원해야 하는 클래스일 때 equals 메서드를 오버라이딩해야 한다.

어떻게 오버라이딩해야 할까?

자바 API 에 명시된 일반적인 규약을 준수하여서 오버라이딩을 해야 한다.

equal()는 동치 관계를 구현한다.

  1. 반사성(reflexive) : null이 아닌 참조 x 에 대해, x.equals(x)true여야 함

  2. 대칭성(symmetric) : null이 아닌 참조 xy 에 대해,
    x.equals(y)true라면 반드시 y.equals(x)true 여야 함.

  3. 추이성(transitive) : null이 아닌 참조 x , y , z 에 대해,
    x.equals(y)true이고 y.equals(z)true라면 반드시 x.equals(z)true 여야 함.

  4. 일관성(consistent) : null이 아닌 참조 xy 에 대해, equals를 통해 비교되는 정보에 변화가 없다면 x.equals(y)의 결과는 호출 횟수와 상관 없이 동일해야 함.

  5. null이 아닌 참조 x 에 대해, x.equals(null)false 여야 함.

이 규약을 지켜서 Money 클래스에서 equals 를 오버라이딩해보자

@Override
public boolean equals(Object o) {
    if (o == this) { return true; }
    if (!(o instanceof Money)) { return false; }
    Money m = (Money)o;
    return Objects.equals(this.amount, m.amount)
        && Objects.equals(this.currencyCode, m.currencyCode);
}

Objects 클래스의 equals 를 통해 값 비교했음을 유의하라

  • 구성 순서를 보자
    1 == 연산자를 이용해 인자가 자기 자신인지 검사
    2 instanceof 연산자를 이용해 자료형을 확인하고 가능하다면 캐스팅
    3 동등 객체라고 판단할 필드들의 값이 같은지 비교

hashCode 메서드

hashCode 메서드는 인스턴스의 주소 값을 해싱하여 만든 인스턴스만의 고유한 숫자값이다.

Object 클래스의 hashCode()

Object 클래스에 선언된 hashCode를 보면 이렇게 되어있다.

구현부가 없는데 native 키워드가 붙어있다. 이 키워드는 OS에서 자바가 아닌 언어(C나 C++)로 구현된 코드를 나타내는 키워드이다.

왜 함께 오버라이딩해야 할까?

equals 메서드를 오버라이딩하는 클래스에서는 hashCode 메서드를 반드시 오버라이딩해주어야 한다.

그 이유는 hashCode의 일반 규약을 보면 알 수 있다.

  1. 응용프로그램 실행 중 같은 객체의 hashCode를 여러 번 호출하는 경우, equals가 사용하는 정보가 바뀌지 않는 한 언제나 동일한 값을 반환하여야 함. (단, 프로그램 재실행한 경우 일관성이 유지될 필요는 없음)

  2. equals 메서드가 같다고 판정한 두 객체의 hashCode 값은 동일해야 함

  3. equals 메서드가 다르다고 판정한 두 객체의 hashCode 값이 다를 필요는 없음
    (다만 다른 경우 해시 테이블의 성능을 향상시킬 수 있음)

2번 규약 때문에 반드시 재정의해주어야 한다.

equals가 재정의된 Money의 두 객체로 확인해보자

    System.out.println(income.equals(expenses)); // true

    System.out.println(income.hashCode());       // 990368553
    System.out.println(expenses.hashCode());     // 1096979270

incomeexpenses 인스턴스는 같은 객체로 equals 를 통해 판정되었지만 두 hashCode는 다르게 나온다.

두 메서드를 재정의하지 않았을 때 생기는 문제는 Hash를 사용하는 Collection Framework에서 발생한다.

equals만 재정의하고 hashCode를 재정의하지 않았을 때의 문제

    Set<Money> moneys = new HashSet<>();

    moneys.add(new Money(1000, "won"));
    moneys.add(new Money(1000, "won"));

    System.out.println(moneys.size());

1000원이라는 동일한 필드를 가진 객체를 HashSet 자료구조 안에 넣어서 사이즈를 확인해보았다.

Set 자료구조는 중복을 허용하지 않기 때문에
moneys의 size는 당연히 1이라고 예상했지만, 예상과 다르게 2가 출력된다.

두 개의 Money(1000, "won") 은 논리적으로 같다고 정의해놓았지만 hashCode 가 달라서 중복된 데이터가 컬렉션에 추가된 것이다.

이렇게 동작하는 이유는 hash를 사용하는 HashSet, HashMap, HashTable은 객체의 동등성을 비교할 때,

  1. hashCode를 비교하여 다르다면 다른 객체로 판단
  2. hashCode가 같다면 equals를 비교, equals가 다르면 다른 객체로 판단
  3. equals도 같다면 동등 객체로 판단

하기 때문이다.

이러한 동작 과정 때문에 hashCode 가 다르다면 equals 비교도 하기 전에 다른 객체로 판단해버린다.

어떻게 오버라이딩해야 할까?

equals를 이용한 동등성 비교는 일반적으로 객체가 가지는 필드가 같다면 동등한 객체로 판단한다.

그렇다면 hashCode 역시 객체의 필드 값이 같을 경우 같은 해시코드를 갖게 하도록 만들면 된다.

@Override
public int hashCode() {
    return Objects.hash(amount, currencyCode);
}

IDE에서 추천해주는대로 만들어서 재정의하면 된다.

Objects 클래스의 hash 메서드는 매개변수로 주어진 값들을 이용하여 고유한 해시코드를 생성해준다.

객체 자체의 해시코드를 얻어야 하는 경우에는??

이미 hashCode() 메서드를 오버라이딩해버리면 반환 동작이 동등성 비교를 하는 필드들을 기준으로 해시 코드가 생성이 된다.
이러면 객체 자체의 해시코드를 얻을 수 없는데, 이 때에도 객체 자체의 해시코드를 얻는 메서드가 있다.

java.lang.System 클래스의 identityHashCode 메서드를 사용하면 된다.

profile
자바 스프링 공부하는 정리 블로그!

0개의 댓글