개발 기록

> [Java] hashcode() & equals() 메서드 재정의와 관계성 본문

JAVA

> [Java] hashcode() & equals() 메서드 재정의와 관계성

1z 2023. 7. 18. 09:55

 

 

1. HashCode 

해싱 알고리즘에 의해 생성된 정수 값을 반환한다. 객체 해시 코드란 객체를 식별할 하나의 정수 값을 말한다. 

public int hashCode()

 

동일한 객체는 동일한 메모리 주소를 갖는다는 것을 의미하므로, 해시 코드 또한 동일해야 한다.  

 

(1) hash code 규약 

㉮  애플리케이션 실행 하는 동안 equals() 내 정보가 수정되지 않았다면 hashCode() 메서드는는  항상 동일한 값을 반환해야 한다. (Java의 프로그램을 실행할 때 마다 달라지는 것은 상관이 없다.)

㉯ equals(Object) 메서드에 따라 두 객체가 동일한 경우, 두 객체의 hashCode() 반환값도 일치해야 한다.

 

(2) 해시 충돌

: 서로 다른 객체 해시값이 동일한 경우를 말한다.

HashTable은 <key,value> 형태로 데이터를 저장하는데 이 때 해시 함수(Hash Function)을 이용하여 key값을 기준으로 고유한 식별값인 해시값을 만든다. (hashcode가 해시값을 만드는 역할을 한다.) 이 해시값을 버킷(Bucket)에 저장한다. 하지만 HashTable 크기는 한정적이기 때문에 같은 서로 다른 객체라 하더라도 같은 해시값을 갖게 될 수도 있다. 이것을 해시 충돌(Hash Collisions)이라고 한다.
https://www.geeksforgeeks.org/implementing-our-own-hash-table-with-separate-chaining-in-java/

 

 

  해시 충돌 해결 ★

1. 해당 버킷(Bucket)에 LinkedList 형태로 객체를 추가한다.

2. hashCode가 같다면 equals() 메서드로 한 번 더 검사한다.

3. hashCode 재정의

 

 

(3) 해시충돌방지를 위한 hashCode 재정의

아래 방법들로 각 객체가 고유한 해시값을 갖게 할 수 있다.

 

방법 1.  id (식별자)와  모든 필드의 해시코드를 곱한다. 

@Override
public int hashCode() {
    return (int) id * name.hashCode() * email.hashCode();
}

 

  방법 2.  해시 코드를 계산하는 데 사용하는 해싱 알고리즘 향상

: 계산된 해시 코드에 더 많은 고유성을 추가하기 위해 두 개의 소수를 사용한다. 

@Override
public int hashCode() {
    int hash = 7;
    hash = 31 * hash + (int) id;
    hash = 31 * hash + (name == null ? 0 : name.hashCode());
    hash = 31 * hash + (email == null ? 0 : email.hashCode());
    return hash;
}

 

  방법 3. Objects.hash()

Java 7 부터 Objects.hash() 유틸리티 메소드를 사용할 수 있다.

Objects.hash(name, email)
// IntelliJ IDEA 에서 Objects.hash 구현
@Override
public int hashCode() {
    int result = (int) (id ^ (id >>> 32));
    result = 31 * result + name.hashCode();
    result = 31 * result + email.hashCode();
    return result;
}

// Eclipse 에서 Objects.hash 구현
@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((email == null) ? 0 : email.hashCode());
    result = prime * result + (int) (id ^ (id >>> 32));
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
}

 

방법 4. @EqualsAndHashCode 

@EqualsAndHashCode 
public class User {
    // fields and methods here
}.

 

※ 참고 

- 여기서 모든 구현이 숫자 31을 활용하는데  31이 홀수이면서 소수이기 때문이다.  

  (숫자를 비트로 바꾸면, 짝수를 곱하는 것은 Shift 연산과 동일한데, 충돌 가능성이 높아진다.)

- 곱셈은 ​ 아래처럼 비트 단위 이동으로 변경할 수 있다.

31 * i == (i << 5) - i

 

(4) hashCode 재정의와 hashMap 예시 

Map<User,Integer> userMap = new HashMap<>();
userMap.put(new User("KIM", 1), 1);
userMap.put(new User("LEE", 2), 2);
userMap.put(new User("SON", 3), 3);
 
int count = userMap.get(new User("LEE", 2));

 

위 코드를 실행했을 때 count가 2 를 반환 할 것을 예상하지만 결과는 그렇지 않다. NullPointerException 이 발생한다. 이유는 user 객체에 대한 해시값 userMap 에서 찾을 수 없기 때문이다   User 클래스 의 인스턴스를 HashMap 키로 사용하려면 hashCode() 메서드를 재정의하여 동일한 객체는 동일한 hashCode 를 반환하도록 해야한다.

public class User {
// ... 
    @Override
    public final int hashCode() {
        int hash = 17;
        hash = 31 * hash + (name == null ? 0 : name.hashCode());
        hash = 31 * hash + (int) age;
        return hash;
    }

    // equlas 재정의
    @Override
    public boolean equlas() {
           // 생략
    }
}

 

 


 

2. Equals ()  

equals () 메소드는 두 객체의 동일 여부를 나타내며 hashCode () 메서드와 함께 구현된다.

// param : obj- 비교할 참조 개체
// return value :개체가 obj 인수와 동일한 경우 true 그렇지 않으면 false
public boolean equals(Object obj)

 

(1) Equals 메서드 규약

① 재귀(reflexive): an object must equal itself

② 대칭성 (symmetric): x.equals(y) must return the same result as y.equals(x)

③ 전이성(transitive): if x.equals(y) and y.equals(z), then also x.equals(z)

④ 일관성 (consistent): equals() 에 사용된 정보가 수정되지 않은 경우  x.equals(y) 를 반복해서 호출하여도 같은 값을 반환해야한다. 

⑤ null-아님 : x.equals(null)는 false

 

 

(2) equals() 재정의  

@Override로 재정의 하지 않으면 그 클래스의 인스턴스는 오직 자기 자신과만 같게 된다. 아래 예제로 확인해볼 수 있다. 

User user1 = new User("KIM", 1);
User user2 = new User("LEE", 2);
boolean result = user1.equals(user2)

 

Income.equals(expenses)가 true를 반환할 것 으로 예상되지만, false 를 반환한다.

그 이유는 equals() 메서드의 기본 구현 로직을 보면 알 수 있다. Object 클래스 에서 equals() 의 기본 구현은 객체의 ID를 비교하는데 주소값이 다른 객체는 서로 다른 객체로 판단한다.

// equlas 기본 구현
public boolean equals(Object obj) {
        return (this == obj);
}

 

객체가 같은지가 아니라 값이 같은지 알고 싶다면 equals 메서드를 재정의 해야 한다. ★

 

※ equals() 를 재정의하지 않아도 되는 경우

* 인스턴스가 둘 이상 만들어지지 않음을 보장하는 클래스 (ex) Enum)

: 이런 클래스에서는 논리적으로 같은 인스턴스가 2개 이상 만들어지지 않는다. 따라서 논리적 동치성과 객체 식별성이 사실상 똑같은 의미가 된다.Object의 equals가 논리적 동치성까지 확인해준다고 볼 수 있다.

 

 

(3)  equals 메서드 구현 방법

@Override
public boolean equals(Object o) {
// == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
    if (o == this)
        return true;
// instanceof 연산자로 입력이 올바른 타입인지 확인한다.
    if (!(o instanceof Money))
        return false;
// 입력을 올바른 타입으로 형변환한다.
    Money other = (Money)o;
// 핵심 필드 일치 검사    
    boolean nameEquls = (this.name == null && other.name == null)
          || (this.name != null && this.name.equals(other.name));
    
    return this.age == other.age && nameEquls;
}

 

 

★ equals() 대칭 위반

Parent parent = new Parent(1);
Child child = new Child(1);

child.equals(parent)   // false 
parent.equals(child) // true

 

.equals ()를 재정의한 클래스를 상속 받을 때, 부모 클래스와 자식 클래스 간의 equlas() 대칭 위반이 발생하는데 이 때는 상속으로 하위 클래스를  만드는 대신 부모 클래스 속성을 사용하면 된다.

class Child {

    private Parent value;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Child))
            return false;
        Child other = (Child) o;
        // Parent 속성을 사용하여 equals 비교를 한다.
        boolean valueEquals = (this.value == null && other.value == null)
          || (this.value != null && this.value.equals(other.value));
        return valueEquals && this.age == other.age;
    }
}

 

 


3. equals () 재정의와 hashcode() 의 관계  

: .equals()와 .hashCode() 둘 다 재정의하거나  둘 다 재정의하지 않아야한다.

: 만일 한쪽만 재정의 하고 같은 값을 가진 개체를 비교 하게 된다면 equals() 반환값은 true 인데 해시 코드는 다르거나, equals() 반환값은 false 인데 해시코드는 일치하는 문제가 발생한다.  

 

 

 

 

 

참고

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

https://www.baeldung.com/java-equals-hashcode-contracts#3-violatingequals-symmetry-with-inheritance

https://velog.io/@mooh2jj/equals%EC%99%80-hashCode%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80