개발자는 기록이 답이다

🐙 equals와 hashcode 메소드의 역할과 사용 방법 본문

언어/Java

🐙 equals와 hashcode 메소드의 역할과 사용 방법

slow-walker 2023. 12. 21. 14:06

 

Effective Java를 공부하다보면 아래와 같은 파트가 있다. equals와 hashCode에 대해 디테일하게 알아보는 시간을 갖고자한다.

 

'equals는 일반 규약을 지켜 재정의하라'

'equals를 재정의하려거든 hashCode()도 재정의하라'


equals와 hashcode 메소드에 대해 알아보자

 

equals와 hashCode는 모든 Java 객체의 부모 객체인 Object 클래스에 정의되어 있다.
그렇기 때문에 Java의 모든 객체는 Object 클래스에 정의된 equals와 hashCode 함수를 상속받고 있다.
 따라서, 클래스에서 이 메소드들을 사용하거나 오버라이드하여 재정의할 수 있다.


🚩 equals란 ?

  • 2개의 객체가 동일한지 검사하기 위해 사용된다.
  • 2개의 객체가 가리키는 곳이 동일한 메모리 주소일 경우에만 동일한 객체가 된다.


🚩 hashCode란 ?

  • 실행 중에(Runtime) 객체의 유일한 integer값을 반환한다.
  • Object 클래스에서는 heap에 저장된 객체의 메모리 주소를 반환하도록 되어있다.
  • 동일한 객체는 동일한 메모리 주소를 갖는다는 것을 의미하므로, 동일한 객체는 동일한 해시코드를 가져야 한다.
  • 그렇기 때문에 우리가 equals() 메소드를 오버라이드 한다면, hashCode() 메소드도 함께 오버라이드 되어야 한다.
  • 두 객체가 equals()에 의해 동일하다면, 두 객체의 hashCode() 값도 일치해야 한다.
  • 두 객체가 equals()에 의해 동일하지 않다면, 두 객체의 hashCode() 값은 일치하지 않아도 된다.

 

hashCode() 메소드에서 반환하는 값은 어떤 원리에 의해 결정될까?

Hashcode를 사용하는 이유 중에 하나는, 객체를 비교할 때 드는 비용을 낮추기 위해서다.

hashcode를 이용하여 객체를 비교하면 equals()를 이용하는 것보다 시간이 단축된다.

 

hashCode() 의 메서드 원리

 

hashCode() 메서드는 객체의 해시 코드를 반환하는데, 이 코드는 객체의 식별자로 사용됩니다.

Java에서 해시 코드는 int 형식으로 반환되며, 일반적으로 객체가 서로 다르면 다른 해시 코드를 갖도록 구현되어 있다.

해시 코드의 주요 목적은 해시 기반 컬렉션에서 빠른 검색을 가능케 하는 데에 있다.

예를 들어, HashMap에서 객체를 저장하고 검색할 때,hashcode를 이용하여 객체를 매핑하면 해당 객체의 위치를 빠르게 찾을 수 있다.

  • hashcode가 다르면, 두개의 객체가 같지 않다
  • hashcode가 같으면, 두개의 객체가 같거나 다를 수 있다

hashCode() 구현 원리

 

해시 코드의 구현은 객체의 속성을 기반으로 해야 하며, 동일한 객체는 항상 동일한 해시 코드를 반환해야 한다.

그러나 두 개의 다른 객체가 동일한 해시 코드를 반환하는 해시 충돌이 발생할 수도 있다.

이는 다른 객체들이 같은 해시 코드를 갖는 상황을 말한다.

보통은 객체의 필드를 조합하여 해시 코드를 생성한다.

예를 들어, String 클래스의 hashCode() 구현은 문자열의 각 문자에 대한 ASCII 코드를 사용하여 해시 코드를 계산한다.

hashCode() 구현 예시

 

@Override
public final int hashCode() {
    int hash = 17;
    if (city != null) {
        result = 31 * hash + city.hashCode();
    }
    if (department != null) {
        result = 31 * hash + department.hashCode();
    }
    return result;
}

 

이 예시에서는 hash 변수를 초기값 17로 설정하고, 각 필드의 해시 코드를 계산한 후에 이전 결과에 31을 곱하고 더하는 방식을 사용한다. 이렇게 함으로써 여러 필드를 조합하여 최종 해시 코드를 얻을 수 있다.

 

※ hashcode를 계산할 때 왜 31을 곱하는지 StackOverflow - Why does Java's hashCode() in String use 31 as a multiplier?에 질문과 답변이 있습니다.

 

public class HashCodeExample {

    public static class Foo {
        private String name;
        private int age;

        public Foo(String name, int age) {
            this.name = name;
            this.age = age;
        }

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

    public static void main(String[] args) {
        Foo foo = new Foo("John", 30);
        System.out.println(foo.hashCode());

        Foo foo2 = new Foo("Doe", 35);
        System.out.println(foo2.hashCode());
    }
}

//OUTPUT :
//2322196
//76702

해시 충돌은 어떻게 처리될까?

 

해시 충돌은 해시 함수의 특성상 어쩔 수 없이 발생할 수 있는 현상이다.

Java의 해시 코드 구현에서는 다른 객체들이 동일한 해시 코드를 반환하는 경우에 대비하여 내부적으로 처리한다.

예를 들어,

HashMap에서는 해시 충돌이 발생하면 해당 위치에 있는 엔트리들을 체인 형태(연결리스트-Linked List)로 연결하여 저장한다.

이를 체이닝이라고 부르며, 동일한 해시 코드를 갖는 다양한 객체들을 하나의 버킷 안에서 관리한다.
HashCode 기반의 비교를 한 후, Equals()가 false라면 찾거나 리스트가 끝날 때까지 다음 노드를 탐색하게 된다.

  • Java에서는 hashCode() 값이 같다고 해서 반드시 두 객체가 동일한 것은 아니다.
  • 추가적으로 equals() 메서드를 사용하여 두 객체가 실제로 동일한지를 비교해야 한다.

출처 : https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/

 

equals() 메서드를 오버라이드할 때 주의할 점은?


equals() 메서드를 오버라이드할 때에는 반드시 일부 규칙을 따라야 하며, hashCode()와의 일관성도 중요하다. 

두 메서드 간의 관계를 유지하면 해시 기반 컬렉션에서 객체를 올바르게 다룰 수 있으며, equals()와 hashCode()를 적절하게 구현함으로써 프로그램의 성능과 정확성을 향상시킬 수 있다.

 

1. Reflexive (반사성)

  • x.equals(x)는 항상 true여야 합니다. 객체는 자기 자신과 동일합니다.

2. Symmetric (대칭성)

  • x.equals(y)가 true를 반환하면 y.equals(x)도 반드시 true여야 합니다. 두 객체 간의 동등성은 양방향으로 성립되어야 합니다.

3. Transitive (추이성)

  • x.equals(y)와 y.equals(z)가 모두 true이면, x.equals(z)도 반드시 true여야 합니다. 동등성은 전이적이어야 합니다.

4. Consistent (일관성)

  • 객체의 상태가 변경되지 않는 한, x.equals(y) 호출 결과는 항상 동일해야 합니다.

5. Non-nullity (비널성)

  • x.equals(null)은 항상 false여야 합니다.

 

hashCode()와 equals() 간의 관계?

 

hashCode()와 equals() 메서드는 서로 밀접한 관계를 가지고 있다.


1. Internal Consistency (내부 일관성)

  • hashCode()의 값은 객체의 내용이 변경되지 않는 한 일관되어야 합니다. 즉, 동일한 객체에 대해 여러 번 호출된 경우 항상 동일한 해시 코드를 반환해야 합니다.

2. equals Consistency (equals() 일관성)

  • 두 객체가 서로 동등하다면(equals()가 true를 반환한다면), 두 객체의 hashCode() 값도 반드시 동일해야 합니다.

3. Handling Collisions (충돌 처리)

  • 서로 다른 객체가 동일한 해시 코드를 갖는 충돌이 발생할 수 있습니다. 이 경우 equals() 메서드를 사용하여 실제로 두 객체가 동일한지 여부를 확인해야 합니다.

 

🤔 느낀점

나는 문자열을 비교하는 경우에서 equals메소드를 사용만 해봤고, 오버라이딩을 별로 사용해본적이 없는 것 같다.

이번 시간을 통해 객체의 동등성을 정의하기 위해 equals를 오버라이딩한다는 것을 자세히 살펴보았다.

 equals메소드는 객체의 메모리 주소를 비교하는데, 논리적으로 동등한 두 객체가 메모리상에서 다른 위치에 저장될 수 있기 때문에

컴퓨터 관점이 아니라 사용자 관점에서 재정의가 필요한 경우에 오버라이딩을 하는 것이다!!

import java.util.Objects;

class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }
    
    // 오버라이딩을 하지 않은 경우
}

 class Main {
    public static void main(String[] args) {
        Person p1 = new Person("홍길동");
        Person p2 = new Person("홍길동"); // 동명이인

        System.out.println(p1.equals(p2)); // false
    }
}

 

equals의 오버라이딩 이해 예제

import java.util.Objects;

class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

	// 오버라이딩을 한 경우
    // 객체 주소 비교가 아닌 Person 객체의 사람 이름이 동등한지 비교로 재정의 하기 위해 오버라이딩
    public boolean equals(Object o) {
        if (this == o) return true; // 만일 현 객체 this와 매개변수 객체가 같을 경우 true
        if (!(o instanceof Person)) return false; // 만일 매개변수 객체가 Person 타입과 호환되지 않으면 false
        Person person = (Person) o; // 만일 매개변수 객체가 Person 타입과 호환된다면 다운캐스팅(down casting) 진행
        return Objects.equals(this.name, person.name); // this객체 이름과 매개변수 객체 이름이 같을경우 true, 다를 경우 false
    }
}

 class Main {
    public static void main(String[] args) {
        Person p1 = new Person("홍길동");
        Person p2 = new Person("홍길동"); // 동명이인

        System.out.println(p1.equals(p2)); // true
    }
}

 

만일 hashCode메서드를 함께 오버라이딩 하지 앟는다면, 해시 기반의 컬렉션을 사용할 때 같은 객체로 인식되지 않는 문제가 발생하기 때문에, 꼭 같이 재정의 헤줘야 한다는 것도 함께 배우게 되었다.

//// hashCode를 오버라이딩 하지 경우 /////

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

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

    public MyClass(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true; // 같은 객체인 경우 true 반환
        if (obj == null || getClass() != obj.getClass()) return false; // null 또는 다른 클래스의 객체인 경우 false 반환

        MyClass myObject = (MyClass) obj;

        // 논리적 동등성을 정의하는 비교 로직
        return id == myObject.id && name.equals(myObject.name);
    }
}

class Main {
    public static void main(String[] args) {
        MyClass obj1 = new MyClass(1, "홍길동");
        MyClass obj2 = new MyClass(1, "홍길동"); // 동명이인

        System.out.println(obj1.equals(obj2)); // true

        Set<MyClass> mySet = new HashSet<>();
        mySet.add(obj1);

        // obj1을 찾을 수 없다면 예상치 못한 동작이 발생할 수 있음
        System.out.println(mySet.contains(obj2)); // false
    }
}
//// hashCode를 오버라이딩 한 경우 /////


import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

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

    public MyClass(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true; // 같은 객체인 경우 true 반환
        if (obj == null || getClass() != obj.getClass()) return false; // null 또는 다른 클래스의 객체인 경우 false 반환

        MyClass myObject = (MyClass) obj;

        // 논리적 동등성을 정의하는 비교 로직
        return id == myObject.id && name.equals(myObject.name);
    }

    // hashCode 메서드도 함께 오버라이딩해야 함
    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

class Main {
    public static void main(String[] args) {
        MyClass obj1 = new MyClass(1, "홍길동");
        MyClass obj2 = new MyClass(1, "홍길동"); // 동명이인

        System.out.println(obj1.equals(obj2)); // true

        Set<MyClass> mySet = new HashSet<>();
        mySet.add(obj1);
        
        System.out.println(mySet.contains(obj2)); // true
    }
}

 


equals 는 왜 overriding 해야하는가

hashCode구현 예제

해시 충돌 방법

equals와 hascode를 반드시 재정의해야 하는 이유!

Hash와 해시 충돌 방법

HashMap의 동작원리

Java HashMap의 동작 원리