자바 equals() 및 hashCode()

Java equals()와 hashCode() 메서드는 Object 클래스에 포함되어 있습니다. 따라서 모든 Java 클래스는 equals()와 hashCode()의 기본 구현을 얻게 됩니다. 이 글에서는 Java equals()와 hashCode() 메서드에 대해 자세히 살펴보겠습니다.

Java equals()

Object 클래스는 equals() 메서드를 다음과 같이 정의합니다:

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

equals() 메서드의 Java 문서에 따르면, 구현은 다음 원칙을 따라야 합니다.

  • 어떤 객체 x에 대해, x.equals(x)true를 반환해야 합니다.
  • 두 객체 x와 y에 대해, x.equals(y)true를 반환하면, y.equals(x)true를 반환해야 합니다.
  • 여러 객체 x, y, z에 대해, x.equals(y)true를 반환하고 y.equals(z)true를 반환한다면, x.equals(z)true를 반환해야 합니다.
  • x.equals(y)의 여러 번 호출은, equals() 메서드 구현에 사용되는 객체 속성 중 하나가 수정되지 않는 한 동일한 결과를 반환해야 합니다.
  • Object 클래스의 equals() 메서드 구현은 두 참조가 동일한 객체를 가리킬 때에만 true를 반환합니다.

Java hashCode()

Java Object hashCode()는 네이티브 메서드이며 객체의 정수 해시 코드 값을 반환합니다. hashCode() 메서드의 일반적인 계약은 다음과 같습니다:

  • hashCode()의 여러 호출은 객체의 equals() 메서드에서 사용되는 속성이 수정되지 않는 한 동일한 정수 값을 반환해야 합니다.
  • 객체의 해시 코드 값은 동일한 애플리케이션의 여러 실행에서 변경될 수 있습니다.
  • 두 개의 객체가 equals() 메서드에 따라 동일하다면, 그들의 해시 코드는 동일해야 합니다.
  • 두 개의 객체가 equals() 메서드에 따라 상이하다면, 그들의 해시 코드는 다르지 않아도 됩니다. 해시 코드 값은 동일하거나 동일하지 않을 수 있습니다.

equals() 및 hashCode() 메서드의 중요성

Java의 hashCode() 및 equals() 메서드는 데이터를 저장하고 검색하기 위해 해시 테이블 기반 구현에서 사용됩니다. 이에 대해 더 자세히 설명한 내용은 Java에서 HashMap 작동 원리에서 설명했습니다. equals() 및 hashCode()의 구현은 다음 규칙을 따라야 합니다.

  • 만약 o1.equals(o2)이면, o1.hashCode() == o2.hashCode()는 항상 true여야 합니다.
  • o1.hashCode() == o2.hashCode가 true인 경우, o1.equals(o2)true인 것은 아닙니다.

equals()와 hashCode() 메소드를 오버라이드해야 하는 경우는 언제인가요?

equals() 메소드를 오버라이드할 때는 거의 반드시 hashCode() 메소드도 오버라이드해야 합니다. 이렇게 해야만 우리의 구현이 그들의 규약을 위반하지 않습니다. equals()와 hashCode() 규약을 위반한다면 프로그램은 예외를 throw하지 않습니다. 클래스를 해시 테이블 키로 사용하지 않을 경우 문제가 발생하지 않습니다. 그러나 클래스를 해시 테이블 키로 사용하려고 한다면 equals()와 hashCode() 메소드를 모두 오버라이드해야 합니다. 기본 구현에 의존하여 equals()와 hashCode() 메소드를 사용하고 커스텀 클래스를 HashMap의 키로 사용할 때 어떤 일이 일어나는지 살펴보겠습니다.

package com.journaldev.java;

public class DataKey {

	private String name;
	private int id;

        // getter와 setter 메소드

	@Override
	public String toString() {
		return "DataKey [name=" + name + ", id=" + id + "]";
	}

}
package com.journaldev.java;

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

public class HashingTest {

	public static void main(String[] args) {
		Map<DataKey, Integer> hm = getAllData();

		DataKey dk = new DataKey();
		dk.setId(1);
		dk.setName("Pankaj");
		System.out.println(dk.hashCode());

		Integer value = hm.get(dk);

		System.out.println(value);

	}

	private static Map<DataKey, Integer> getAllData() {
		Map<DataKey, Integer> hm = new HashMap<>();

		DataKey dk = new DataKey();
		dk.setId(1);
		dk.setName("Pankaj");
		System.out.println(dk.hashCode());

		hm.put(dk, 10);

		return hm;
	}

}

위의 프로그램을 실행하면 null이 출력됩니다. 이는 Object의 hashCode() 메소드가 버킷을 찾기 위해 사용되기 때문입니다. HashMap의 키에 접근할 수 없으며 데이터를 검색하기 위해 키를 다시 생성하고 있기 때문에 두 개의 객체의 해시 코드 값이 다르기 때문에 값을 찾을 수 없습니다.

equals()와 hashCode() 메소드 구현

우리는 직접 equals()와 hashCode() 메소드를 구현할 수 있습니다. 그러나 이를 조심스럽게 구현하지 않으면 런타임에 이상한 문제가 발생할 수 있습니다. 다행히 요즘 대부분의 IDE는 이를 자동으로 구현하고 필요에 따라 우리의 요구에 맞게 변경할 수 있는 방법을 제공합니다. 우리는 Eclipse를 사용하여 equals()와 hashCode() 메소드를 자동으로 생성할 수 있습니다. 다음은 자동으로 생성된 equals()와 hashCode() 메소드의 구현입니다.

@Override
public int hashCode() {
	final int prime = 31;
	int result = 1;
	result = prime * result + id;
	result = prime * result + ((name == null) ? 0 : name.hashCode());
	return result;
}

@Override
public boolean equals(Object obj) {
	if (this == obj)
		return true;
	if (obj == null)
		return false;
	if (getClass() != obj.getClass())
		return false;
	DataKey other = (DataKey) obj;
	if (id != other.id)
		return false;
	if (name == null) {
		if (other.name != null)
			return false;
	} else if (!name.equals(other.name))
		return false;
	return true;
}

equals()와 hashCode() 메소드 모두 계산에 동일한 필드를 사용하여 계약이 유효하도록 유지되었음을 주목하세요. 테스트 프로그램을 다시 실행하면 맵에서 객체를 가져와 프로그램이 10을 출력합니다. 우리는 또한 equals와 hashCode 메소드의 구현을 자동으로 생성하기 위해 Project Lombok을 사용할 수 있습니다.

해시 충돌이란 무엇인가요?

매우 간단하게 말하면, Java 해시 테이블 구현은 get 및 put 연산에 다음 로직을 사용합니다.

  1. 먼저 “키” 해시 코드를 사용하여 사용할 “버킷”을 식별합니다.
  2. 동일한 해시 코드를 가진 버킷에 객체가 없는 경우, put 연산을 위해 객체를 추가하고 get 연산에는 null을 반환합니다.
  3. 만약 버킷에 동일한 해시 코드를 가진 다른 객체가 있다면, “key” equals 메소드가 작용합니다.
    • equals()가 true를 반환하고 put 작업인 경우, 객체 값이 덮어씌워집니다.
    • equals()가 false를 반환하고 put 작업인 경우, 새로운 항목이 버킷에 추가됩니다.
    • equals()가 true를 반환하고 get 작업인 경우, 객체 값이 반환됩니다.
    • equals()가 false를 반환하고 get 작업인 경우, null이 반환됩니다.

아래 이미지는 HashMap의 버킷 항목과 그들의 equals() 및 hashCode()의 관계를 보여줍니다. 두 키가 동일한 해시 코드를 가질 때 발생하는 현상을 해시 충돌이라고 합니다. hashCode() 메소드가 올바르게 구현되지 않은 경우, 더 많은 해시 충돌이 발생하고 맵 항목이 제대로 분산되지 않아 get 및 put 작업이 느려질 수 있습니다. 이것이 모든 버킷에 맵 항목이 제대로 분산되도록 소수를 사용하는 이유입니다.

만약 hashCode()와 equals() 모두 구현하지 않으면 어떻게 될까요?

위에서 이미 보았듯이, hashCode()가 구현되지 않으면 값을 검색할 수 없습니다. 왜냐하면 HashMap은 항목을 찾기 위해 해시 코드를 사용하여 버킷을 찾기 때문입니다. 만약 hashCode()만 사용하고 equals()를 구현하지 않으면 값도 검색되지 않을 것입니다. 왜냐하면 equals() 메소드는 false를 반환하기 때문입니다.

equals()와 hashCode() 메소드를 구현하는 데 대한 모범 사례

  • equals()와 hashCode() 메소드 구현에서 동일한 속성을 사용하여 계약이 위배되지 않도록합니다. 어떤 속성이 업데이트되면 문제가 발생하지 않도록합니다.
  • 해시 테이블 키로 불변 객체를 사용하는 것이 좋습니다. 이렇게하면 매 호출마다 해시 코드를 계산하는 대신 해시 코드를 캐시 할 수 있습니다. 그래서 String은 해시 테이블 키로 좋은 후보입니다. 그것은 불변이며 해시 코드 값을 캐시합니다.
  • hashCode() 메소드를 구현하여 가능한 한 적은 수의 해시 충돌이 발생하고 항목이 모든 버킷에 고르게 분포되도록합니다.

완전한 코드를 GitHub 저장소에서 다운로드할 수 있습니다.

Source:
https://www.digitalocean.com/community/tutorials/java-equals-hashcode