equals 와 hashcode 를 함께 정의해야 하는 이유는?
🟧 equals ?
Object 클래스에 있는 equals() 메서드.
두 개의 객체가 동일한지 확인하기 위해 사용한다.
equals() 메서드를 보면 두 객체가 동일한 메모리 주소를 가리키는지 확인하고 있다.
public class Object {
public boolean equals(Object obj) {
// 동일한 메모리 주소인지 확인한다.
return (this == obj);
}
}
🟢 case01. equals 오버라이딩 하지 않은 경우
Name 객체의 내부 데이터가 같아도 equals로 비교하면 메모리 주소가 다르다.
이름01과 이름02는 동일하지 않다고 판단된다.
@DisplayName("같은 값을 가져도 동일하지 않다고 판단된다.")
@Test
void equals_not_override() {
final var 이름01 = new Name("hyena");
final var 이름02 = new Name("hyena");
// 이름01과 이름02는 동일하지 않다.
assertThat(이름01).isNotEqualTo(이름02);
}
class Name {
private final String value;
public Name(final String value) {
this.value = value;
}
}
case 02. equals 오버라이딩한 경우
Name 클래스의 equals 메서드를 오버라이딩하면 메모리 주소로 비교하지 않을 수 있다.
클래스 내의 필드 값을 비교해서 동일한지 판단할 수 있다.
@DisplayName("같은 값을 가지면 동일하다고 판단된다.")
@Test
void equals_override() {
final var 이름01 = new Name_Override("hyena");
final var 이름02 = new Name_Override("hyena");
// 이름01과 이름02는 동일하다.
assertThat(이름01).isEqualTo(이름02);
}
class Name_Override {
private final String value;
public Name_Override(final String value) {
this.value = value;
}
// IntelliJ equals 기본 재정의 사용
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final Name_Override that = (Name_Override) o;
return Objects.equals(value, that.value);
}
}
🟧 hashcode ?
Object 클래스에 있는 hashCode 메서드.
hashCode() 메서드의 반환값은 객체를 식별하는 Integer 값이다.
알고리즘을 적용하여 나온 Integer 값을 hashcode라고 한다.
public class Object {
@HotSpotIntrinsicCandidate
public native int hashCode();
}
🟢 case01. hashcode 오버라이딩 하지 않은 경우
hashcode 메서드는 보통 객체 마다 다른 값을 반환한다.
때문에 두 객체는 각각 다른 값을 반환한다.
@DisplayName("보통 객체 마다 다른 값을 반환한다.")
@Test
void hashCode_not_override() {
final var 이름01 = new Name("hyena");
final var 이름02 = new Name("hyena");
final var 이름01_해쉬코드 = System.identityHashCode(이름01);
final var 이름02_해쉬코드 = System.identityHashCode(이름02);
System.out.println("이름01_해쉬코드 = " + 이름01_해쉬코드);
System.out.println("이름02_해쉬코드 = " + 이름02_해쉬코드);
assertThat(이름01_해쉬코드).isNotEqualTo(이름02_해쉬코드);
}
class Name {
private final String value;
public Name(final String value) {
this.value = value;
}
}
🟢 case02. hashcode 오버라이딩한 경우
hashcode를 오버라이딩하여 필드의 값을 통해서 나올 수 있도록 한다.
@DisplayName("모든 필드 값이 같을 경우 hashcode 메서드가 같은 값을 반환한다.")
@Test
void hashCode_override() {
final var 이름01 = new Name_Override("hyena");
final var 이름02 = new Name_Override("hyena");
final var 이름01_해쉬코드 = 이름01.hashCode();
final var 이름02_해쉬코드 = 이름02.hashCode();
System.out.println("이름01 = " + 이름01_해쉬코드);
System.out.println("이름02 = " + 이름02_해쉬코드);
assertThat(이름01_해쉬코드).isEqualTo(이름02_해쉬코드);
}
class Name_Override {
private final String value;
public Name_Override(final String value) {
this.value = value;
}
@Override
public int hashCode() {
return value.hashCode();
}
}
🟧 eqauls와 hashcode를 같이 재정의해야 하는 이유
hashcode 메서드는 hash값을 이용하는 자료구조를 이용할 때 사용된다.
해당 자료구조를 이용할 때 같은 값을 갖는 객체가 동일하다고 판단할거면 재정의하는게 좋다.
이유는 인터페이스 Map의 구현체인 HashMap의 put 메서드를 살펴보면 알 수 있다.
🟢 HashMap
HashMap의 put 메서드는 putVal 메서드를 호출하고 있다.
public class HashMap<K, V> {
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
}
putVal 메서드를 보면
인덱스 버킷이 비어있지 않을 경우에
equals, hash가 같을 경우에 동일하다고 판단하고 있다.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 처음으로 들어가는 값일 경우 테이블 사이즈를 초기화한다.
if ((tab = table) == null || (n = tab.length) == 0) {
n = (tab = resize()).length;
}
// 처음 들어가는 값일 경우 Node를 추가한다.
if ((p = tab[i = (n - 1) & hash]) == null) {
tab[i] = newNode(hash, key, value, null);
}
// === equals & hashcode ===
// 인덱스 버킷이 비어있지 않은 경우
// hash와 equals가 같을 경우 동일하다고 판단한다.
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) {
e = p;
}
이러한 이유로 같은 값을 갖는 객체를 동일하다고 판단했다면 equals, hashcode를 같이 정의해주는 편이 좋다.
'💬 언어' 카테고리의 다른 글
[JVM] Java Virtual Machine Architecture (0) | 2023.07.02 |
---|---|
동일성과 동등성의 차이 (0) | 2023.03.20 |