[Java] equals와 hashCode

j-yong98·2026년 2월 8일

Java

목록 보기
3/7

equalshashCode는 Java에서 객체 비교와 해시 기반 자료구조 동작의 핵심이다. 이번에 equalshashCode 에 대해 알아보자.

equals

Java에서 equals는 두 객체가 논리적으로 동일한지를 판단하기 위해 사용되는 메서드이다.

기본적으로 Object 클래스의 equals는 객체의 참조(주소)를 비교한다. 즉, 두 객체가 동일한 인스턴스일 때만 true를 반환한다.

Person p1= new Person("가나다",10);
Person p2= new Person("가나다",10);

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

위 예제에서 두 객체는 동일한 필드 값을 가지고 있지만, 서로 다른 인스턴스이기 때문에 기본 equals 구현에서는 false를 반환한다.

하지만 많은 경우 객체의 실제 값을 기준으로 동등성을 판단해야 한다. 이때 equals 메서드를 재정의하여 원하는 기준으로 비교할 수 있다.

@Override
public boolean equals(Object o) {
  if (o == null || getClass() != o.getClass()) {
    return false;
  }
  Person person = (Person) o;
  return age == person.age && Objects.equals(name, person.name);
}

이처럼 equals를 재정의하면 객체의 참조가 아닌 상태를 기준으로 동등성을 판단할 수 있다. 이러한 동등성 비교는 객체의 실제 값이 같은지 확인하거나 해시 기반 자료구조에서 hashCode와 함께 사용된다.

hashCode

Object 클래스의 hashCode는 다음과 같이 정의되어 있다.

Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap.

즉, 객체의 해시 코드 값(정수)을 반환하며, HashMap과 같은 해시 기반 자료구조에서 효율적인 탐색을 돕기 위해 사용된다.

hashCode는 다음과 같은 규칙을 따른다.

  • 동일 객체는 실행 중 항상 같은 hashCode를 반환해야 한다.
    equals 비교에 사용되는 값이 변경되지 않았다면, 해시 기반 자료구조에서 동일한 위치(bucket)를 찾을 수 있어야 하기 때문이다.
  • equals가 true라면 hashCode는 반드시 같아야 한다.
    같은 객체가 서로 다른 bucket에 저장되면 해시 자료구조가 정상적으로 동작하지 않기 때문이다.
  • equals가 false라고 해서 hashCode가 반드시 달라야 하는 것은 아니다.
    서로 다른 객체라도 같은 해시 값이 나올 수 있으며, 이를 해시 충돌이라고 한다. 충돌이 적을수록 성능에는 유리하다.

equals와 hashCode

아래 예제를 통해 equals와 hashCode에 대해 알아보자. Set은 중복을 허용하지 않는 자료구조이다.

  public static void main(String[] args) {
    Person p1 = new Person("가나다", 10); 
    Person p2 = new Person("가나다", 10);

    System.out.println(p1.hashCode()); // 1747585824
    System.out.println(p2.hashCode()); // 1023892928

    Set<Person> set = new HashSet<>();
    set.add(p1);
    set.add(p2);

    System.out.println(set.size()); // 2
  }

equalshashCode를 재정의하지 않은 경우, 두 객체는 서로 다른 객체로 인식된다.

기본적으로 객체의 주소 값을 기반으로 생성된 해시 값을 반환하도록 설계되어 있어, 동일한 필드 값을 가지고 있어도 서로 다른 객체로 처리된다.

hashCode만 재정의

...
System.out.println(p1.hashCode()); // 1356622847
System.out.println(p2.hashCode()); // 1356622847
...
System.out.println(set.size()); // 2

위 결과에서 두 객체는 동일한 hashCode를 가지는 것을 확인할 수 있다.

하지만 HashSet에서는 서로 다른 객체로 판단되어 Set의 크기가 2가 된다.

그 이유는 다음 순서로 비교하기 때문이다.

  1. hashCode로 bucket 위치 결정
  2. 같은 bucket 안에서 equals 비교 수행

hashCode가 같아 동일한 bucket에 들어가더라도, equals가 false라면 서로 다른 객체로 판단된다.

equals만 재정의

...
System.out.println(p1.hashCode()); // 1747585824
System.out.println(p2.hashCode()); // 1023892928
...
System.out.println(set.size()); // 2

equals만 재정의한 경우에도 Set의 크기는 2가 된다.

HashSet은 먼저 hashCode로 bucket을 결정하기 때문에, 서로 다른 hashCode를 가진 객체는 애초에 다른 bucket에 저장된다.

String의 hashCode

일반적으로 hashCode는 객체의 필드 값을 기반으로 생성한다.
그렇다면 String은 어떤 방식으로 hashCode를 계산할까?

String a = "abcde";
String b = "abcde";

System.out.println(a.hashCode()); // 92599395
System.out.println(b.hashCode()); // 92599395

String 클래스의 hashCode는 다음과 같은 방식으로 계산된다.

s[0]31^(n-1) + s[1]31^(n-2) + ... + s[n-1]

즉, 문자열의 각 문자 값을 이용해 31을 곱하면서 누적 계산하는 방식이다.
이러한 계산 방식 덕분에 동일한 문자열은 항상 동일한 hashCode 값을 가지게 된다.

왜 31을 사용할까?

hash 계산에 31이라는 숫자가 사용되는 이유는 해시 분포와 성능을 모두 고려한 선택이기 때문이다.

  • 소수(prime number) 31은 소수이기 때문에 해시 값이 비교적 고르게 분포되도록 도와주며, 특정 패턴에서 해시 충돌이 발생할 가능성을 줄여준다.
  • 연산 최적화 31 * x(x << 5) - x 와 같이 비트 연산으로 변환할 수 있어
    계산 비용 측면에서도 효율적이다.

이러한 이유로 String뿐만 아니라 많은 Java 객체의 hashCode 구현에서도
31을 사용하는 패턴을 자주 볼 수 있다.

0개의 댓글