본문 바로가기

공부 정리

equals() 와 hashCode()는 왜 재정의 해야 할까?

반응형

개요


 롬복으로 equals()를 재정의하면 항상 hashCode()도 같이 재정의하도록 안내하고 있습니다. 어노테이션도 @EqualsAndHashCode입니다. 이는 Map, Set 등의 자바 컬렉션 클래스가 equals, compareTo, hashCode를 사용하기 때문입니다. equals와 hashCode를 같이 재정의 하지 않으면 원하는 대로 동작하지 않을 수도 있습니다.(해쉬 테이블에서 키로 사용할 때 기준입니다) 따라서 이번 시간에는 equals와 hashCode를 알아보겠습니다..

 

 

equals()


자바의 equals() 와 hashCode()는 Object 클래스에 있기 때문에, 모든 자바 클래스는 기본적으로 equals()와 hashCode() 구현체를 가집니다. 

equals() 자바 공식문서에 따르면, 다음 규칙을 따라야 합니다.

 

- 어떠한 객체 x는, xe.quals(x)가 항상 참이다
- 어떠한 객체 x,y는 y.equals(x)가 참이면 x.equals(y)가 참이다
- 어떠한 객체 x,y,z는 만약 x.equals(y)가 참이고 y.equals(z)가 참이면, x.equals(z)는 참이다
- equals() 메서드 구현에서 사용되는 객체 속성이 수정되지 않는 한 x.equals(y)를 여러 번 호출하면 동일한 결과를 반환해야 합니다.
- 객체 클래스 equals() 메서드 구현은 두 참조가 동일한 객체를 가리키는 경우에만 참입니다.

 


hashCode()


자바 객체 hashCode()는 기본 메서드이며 개체의 정수 해시 코드 값을 반환합니다.

 

- 만약 equals() 메서드에서 사용되는 개체 속성이 변하지 않는다면, hashCode()를 여러 번 호출해도 동일한 정수 값을 반환해야 합니다.
- 개체 해쉬 코드 값은 같은 어플리케이션의 여러 번 실행에 변경될 수 있습니다
- 2개의 개체가 equals() 메서드 비교에서 똑같다면, 해쉬 코드도 같아야 합니다
- 2개의 개체가 equals() 메서드 비교에서 다르다면, 해쉬 코드가 같은 수도 있고 다를 수도 있습니다.

 

 

 

왜 equals()와 hashCode()를 함께 재정의(오버라이드) 해야 할까?


You must override hashCode() in every class that overrides equals(). Failure to do so will result in a violation of the general contract for Object.hashCode(), which will prevent your class from functioning properly in conjunction with all hash-based collections, including HashMap, HashSet, and Hashtable.

from Effective Java, by Joshua Bloch

 

답은 이펙티브 자바에서도 찾을 수 있습니다. 해쉬 기반의 컬렉션을 사용하는 HashMap, HashSet, Hashtable 등은 모두 해쉬 기반으로 hashCode()와 equals()을 동시에 사용하는데, 만약 하나만 재정의 하는 경우 기존 규칙을 위반하여 원하는 결과가 나오지 않기 때문입니다. 2개의 메서드가 서로 실과 바늘처럼 밀접하게 관련이 되어 있습니다.

 

 

equals()와 hashCode() 코드로 상황 이해하기


 equals() 메서드를 오버라이드 할 때, hashCode()도 오버라이드를 해야 규칙 위반을 방지할 수 있습니다. 만약에 어떤 클래스를 해쉬 테이블 키로 사용하지 않는다면, 굳이 equals()와 hashCode()를 오버라이드를 할 필요가 없습니다. 하지만, 해쉬 테이블 키로 사용한다면, 2개의 메서드를 모두 오버라이드 해야 합니다.

 

코드를 통해서 보겠습니다.

 

public class DataKey {
	private String name;
	private int id;

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

}

 

DataKey 클래스는 name, id라는 변수를 가지고 있습니다. 아래에서 DataKey를 이용하여 HashMap에 활용해 보겠습니다.

 

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;
	}

}

 

 결론적으로, 해당 코드를 실행하면 main()에서 null을 출력하는데 과정은 다음과 같습니다. hashCode() 메서드의 결과값으로 HashMap에서 키가 담긴 '버킷을 탐색'합니다. 하지만 hashCode()의 결과 값이 다르므로 버킷에 접근할 수 없고 키를 다시 생성합니다. 따라서 해쉬 테이블 내부에서 기존의 DataKey 객체를 알지 못하고 새로운 DataKey 객체로 인식합니다.

IDE의 자동완성 기능으로 hashCode() and equals()를 구현한 오버라이드 메서드를 생성해보겠습니다.

 

@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() 메서드는 계산을 위해서 name 변수를 사용하므로 동일하게 DataKey 객체를 인식 할 수 있습니다. 따라서 위의 main 결과는 정상적으로 10을 출력합니다.

 

 

 

해쉬 충돌(Hash Collision)


해쉬 테이블은 다음의 순서로 작업을 합니다.

1. "키" 해쉬 코드를 사용해 사용할 "버킷"을 탐색합니다.
2. 버킷에 같은 해쉬코드를 가진 동일한 객체가 없다면, 해당 객체를 버킷에 넣습니다.
3. 만약, 같은 해쉬코드를 가진 버킷에 다른 객체가 존재한다면, equals() 메서드를 확인합니다

1. equals()가 참이고 추가 행위라면 객체를 오버라이딩합니다
2. equals()가 거짓이고 추가 행위라면 버킷에 새로운 entry가 추가됩니다
3. equals()가 참이고 조회 행위라면 객체 값이 반환됩니다
4. equals()가 거짓이고 조회 행위라면, null이 반환됩니다.

 

 

 

  • equals()와 hashCode()를 구현하지 않는다면?

HashMap은 entry를 찾기 위해 해쉬 코드로 버킷을 탐색하기 때문에 구현하지 않는다면 값을 알 수 없습니다. 또한 hashCode()만 구현해도, equals()는 항상 false를 반환하므로 값을 찾을 수 없습니다. 

 

 

참고

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

반응형

'공부 정리' 카테고리의 다른 글

HashSet은 내부가 어떻게 구현이 되어 있는가?  (0) 2022.10.15
ConcurrentHashMap vs Hashtable vs Synchronized Map  (0) 2022.10.15
HTTP Method for Restful Services  (0) 2022.10.14
대칭키 vs 비대칭키  (0) 2022.09.05
TLS/SSL  (0) 2022.09.05