[JAVA] equals()와 hashCode() 완벽 정리

2025. 2. 24. 17:51·BackEnd/JAVA

자바에서 객체의 동등성 검사는 매우 중요한 개념이며, 이를 위해 equals()와 hashCode() 메서드를 올바르게 이해하고 구현하는 것이 필수적입니다.

  • equals() : 두 객체가 논리적으로 같은지 비교하는 메서드
  • hashCode() : 객체를 빠르게 찾을 수 있도록 해시값(정수)를 반환하는 메서드

이 글에서는 equals()와 hashCode()의 개념, 원칙, 구현 방법, 컬렉션과의 관계, 그리고 실무에서의 활용까지 완벽하게 정리해보겠습니다.

1. eqauls()와 hashCode()란?

1.1 equals()란?

equals()는 두 객체가 논리적으로 같은지 판단하는 메서드입니다.

  • Object 클래스의 기본 equals()는 == 연산자와 동일하게 참조(주소) 비교를 수행합니다.
  • 따라서 equals()를 오버라이딩하지 않으면, 동일한 값을 가진 객체라도 서로 다르다고 판단됩니다.

📌 equals() 기본 구현 (참조 비교)

class Person {
    String name;

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

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

        System.out.println(p1 == p2);       // false (주소 비교)
        System.out.println(p1.equals(p2)); // false (기본 equals()는 주소 비교)
    }
}

위 코드에서 p1.equals(p2)가 false를 반환하는 이유는, Object 클래스의 기본 equals()가 메모리 주소를 비교하기 때문입니다.

📌 equals() 오버라이딩 (논리적 비교)

객체가 동일한 값을 가질 경우 true를 반환하도록 equals()를 재정의할 수 있습니다.

class Person {
    String name;

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return name.equals(person.name);
    }
}

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

        System.out.println(p1.equals(p2)); // true (이제 논리적으로 동일하다고 판단)
    }
}

1.2 hashCode()란?

hashCode()는 객체를 빠르게 검색할 수 있도록 정수 형태의 해시값을 반환하는 메서드입니다.

  • 기본적으로 Object 클래스의 hashCode()는 메모리 주소 기반 해시값을 반환합니다.
  • hashCode()를 적절히 오버라이딩하면, 같은 값을 가진 객체가 동일한 해시코드를 갖도록 만들 수 있습니다.

📌 hashCode() 기본 동작 예시

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

        System.out.println(p1.hashCode()); // 예: 12345678
        System.out.println(p2.hashCode()); // 예: 87654321 (값은 같지만 해시코드는 다름)
    }
}

위 코드에서 p1과 p2는 같은 값을 가지지만, 해시코드는 다를 수 있습니다.

이를 해결하려면 hashCode()를 적절히 재정의해야 합니다.

📌 hashCode() 오버라이딩

@Override
public int hashCode() {
    return Objects.hash(name); // 필드를 기준으로 해시코드 생성
}

이제 name이 동일하면 같은 해시코드를 반환하여, 해시 기반 컬렉션에서 정상적으로 동작할 수 있습니다.

2. equals()와 hashCode()의 관계

equals()와 hashCode()는 객체의 동등성을 검사할 때 반드시 함께 고려해야 하는 메서드입니다.

특히, 해시 기반 컬렉션(HashSet, HashMap, HashTable)을 사용할 때 필수적으로 오버라이딩해야 합니다.

2.1 equals()와 hashCode() 구현 시 반드시 지켜야 할 규칙

1) equals()가 true이면 hashCode()도 같아야 한다.

  • 논리적으로 동일한 객체라면(equals()가 true), 같은 해시코드를 반환해야 합니다.
  • 그렇지 않으면 HashSet, HashMap 같은 컬렉션에서 제대로 동작하지 않습니다.

📌 hashCode()를 재정의하지 않은 경우 (잘못된 코드)

import java.util.HashSet;

class Person {
    private String name;
    private int age;

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

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

public class Main {
    public static void main(String[] args) {
        HashSet<Person> set = new HashSet<>();

        Person p1 = new Person("홍길동", 30);
        Person p2 = new Person("홍길동", 30);

        set.add(p1);
        set.add(p2);

        System.out.println(set.size());  // 예상: 1, 실제: 2 (중복 저장됨)
    }
}
  • p1.equals(p2) == true지만, hashCode()를 오버라이딩하지 않았기 때문에 다른 해시코드를 가지게 됩니다.
  • 따라서 같은 객체임에도 HashSet에 중복 저장됩니다.

📌 hashCode()를 올바르게 오버라이딩한 경우 (올바른 코드)

@Override
public int hashCode() {
    return Objects.hash(name, age);  // 같은 필드를 기준으로 해시코드 생성
}
  • 이제 equals()가 true인 경우, hashCode()도 동일한 값을 반환하여 중복 저장이 방지됩니다.

2) hashCode()가 같다고 해서 equals()가 true를 보장하지 않는다.

  • 서로 다른 객체라도 해시코드 값이 같을 수 있습니다. 이를 해시 충돌(Hash Collision) 이라고 합니다.
  • 이는 자연스러운 현상이지만, 너무 자주 발생하면 해시 기반 컬렉션의 성능이 저하됩니다.

📌 잘못된 예제 (해시 충돌 발생)

class Car {
    private String model;

    public Car(String model) {
        this.model = model;
    }

    @Override
    public int hashCode() {
        return 42;  // 모든 객체가 같은 해시코드를 반환 (심각한 문제!)
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Car car = (Car) obj;
        return model.equals(car.model);
    }
}

public class Main {
    public static void main(String[] args) {
        Car c1 = new Car("Tesla");
        Car c2 = new Car("BMW");

        System.out.println(c1.hashCode());  // 42
        System.out.println(c2.hashCode());  // 42 (같은 해시코드)

        System.out.println(c1.equals(c2));  // false (다른 객체지만 같은 해시코드)
    }
}
  • c1과 c2는 다른 모델이지만, hashCode()가 항상 42를 반환하여 같은 해시코드를 가집니다.
  • 해시 기반 컬렉션에서 성능이 급격히 저하될 수 있습니다.

📌 해결 방법 (해시 충돌 방지)

@Override
public int hashCode() {
    return Objects.hash(model);
}
  • 이제 다른 값은 서로 다른 해시코드를 가지게 되어 해시 충돌 문제 해결!

3) 객체가 변경되지 않는 한, hashCode()는 항상 동일한 값을 반환해야 한다.

  • 객체를 HashSet, HashMap에 넣고 나서 객체의 필드 값을 변경하면 안 됩니다.
  • 필드가 변경되면 hashCode() 값이 바뀌어 컬렉션에서 객체를 찾지 못하는 문제 발생합니다.

📌 잘못된 예제 (해시코드가 변경되는 경우)

import java.util.HashSet;

class Employee {
    private String name;

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

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

    public void setName(String newName) {
        this.name = newName;
    }
}

public class Main {
    public static void main(String[] args) {
        HashSet<Employee> set = new HashSet<>();

        Employee e = new Employee("John");
        set.add(e);

        System.out.println(set.contains(e)); // true

        e.setName("Mike"); // 객체의 필드 값 변경!

        System.out.println(set.contains(e)); // false (예상: true)
    }
}
  • e의 name이 변경되면서 hashCode() 값도 변경되었습니다.
  • 결과적으로 HashSet에서 객체를 찾지 못하는 문제가 발생합니다.

📌 해결 방법 (가변 필드 변경 금지)

  • 해시코드에 사용되는 필드는 변경하지 않는 것이 원칙입니다.
  • 만약 변경해야 한다면, 컬렉션에서 제거 후 다시 추가해야 합니다.

4) equals() 필드를 비교하는 경우, hashCode()도 동일한 필드를 기반으로 생성해야 한다.

  • equals()가 비교하는 필드가 hashCode()에도 포함되어야 한다.

📌 잘못된 예제 (equals()와 hashCode()가 다른 기준 사용)

class Student {
    private String name;
    private int id; // 학번

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

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Student student = (Student) obj;
        return name.equals(student.name); // name만 비교
    }

    @Override
    public int hashCode() {
        return id; // id만 사용 (불일치 문제 발생)
    }
}
  • equals()는 name만 비교하는데, hashCode()는 id를 기반으로 생성됩니다.
  • 동일한 name을 가진 객체라도 서로 다른 해시코드를 가질 가능성이 있습니다.

📌 해결 방법

@Override
public int hashCode() {
    return Objects.hash(name);  // `equals()`가 비교하는 필드와 동일한 필드를 사용
}
  • equals()와 hashCode()가 동일한 기준을 사용하여 일관성 유지합니다.

2.2 equals()와 hashCode()의 동작 순서

equals()와 hashCode()의 동작과정은 다음과 같습니다.

  1. hashCode()를 먼저 호출하여 해시값을 비교합니다.
    • 만약 해시값이 다르면, 두 객체는 다른 객체로 판단됩니다.
    • 만약 해시값이 같으면, 다음 단계로 진행됩니다.
  2. equals()를 호출하여 필드 값을 비교합니다.
    • 만약 equals()가 true를 반환하면, 두 객체는 동등 객체로 판단됩니다.
    • 만약 equals()가 false를 반환하면, 두 객체는 서로 다른 객체로 판단됩니다.

즉, hashCode()가 다르면 equals()는 비교할 필요도 없이 false를 반환하지만, hashCode()가 같아도 equals()가 반드시 true가 되는 것은 아닙니다.

3. identityHashCode()란?

자바에서 hashCode()는 원래 객체의 주소값을 기반으로 해시값을 반환하는 메서드입니다. 그러나, hashCode()를 오버라이딩하여 객체의 필드를 기준으로 해시값을 반환하면, 객체의 원래 주소 기반 해시값을 얻기 어려워질 수 있습니다.

이 문제를 해결하기 위해 자바는 System.identityHashCode(Object obj)를 제공합니다.

이 메서드는 객체의 메모리 주소(참조값)를 기반으로 한 해시코드를 반환합니다.

3.1 identityHashCode() vs hashCode() 차이점

  • 객체의 논리적 동등성을 판단할 때는 hashCode()를 오버라이딩하여 사용
  • 객체의 원래 메모리 주소 기반 해시값이 필요할 때는 System.identityHashCode()를 사용

3.2 identityHashCode()의 동작 방식

아래 예제에서 Person 클래스는 hashCode()를 오버라이딩하여 name 필드를 기준으로 해시값을 반환하도록 구현했습니다.

하지만, System.identityHashCode()를 사용하면 객체의 원래 해시코드(메모리 주소 기반)가 반환되는 것을 확인할 수 있습니다.

📌 hashCode()와 identityHashCode() 비교 예제

import java.util.Objects;

class Person {
    String name;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person person = (Person) o;
        return Objects.equals(this.name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);  // name을 기반으로 해시코드 생성
    }
}

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

        // equals()와 hashCode()를 오버라이딩했기에, 두 객체의 필드 값이 같다면 동일한 해시코드 반환
        System.out.println("p1.hashCode(): " + p1.hashCode()); // 예: 54150093
        System.out.println("p2.hashCode(): " + p2.hashCode()); // 예: 54150093 (같음)

        // 하지만, 실제 객체의 주소값을 알고 싶다면 identityHashCode()를 사용해야 함
        System.out.println("System.identityHashCode(p1): " + System.identityHashCode(p1)); // 예: 622488023
        System.out.println("System.identityHashCode(p2): " + System.identityHashCode(p2)); // 예: 414493378 (다름)
    }
}
  • p1.hashCode()와 p2.hashCode()가 같은 이유는 name 필드를 기준으로 hashCode()를 오버라이딩했기 때문입니다.
  • 그러나, System.identityHashCode(p1)와 System.identityHashCode(p2)는 각각 다른 값을 반환합니다.
  • 이는 identityHashCode()가 객체의 참조값(메모리 주소 기반 해시값)을 반환하기 때문입니다.

3.3 identityHashCode()가 필요한 경우

  1. hashCode()를 오버라이딩했지만 원래 객체의 참조값이 필요한 경우
    • 위의 예제처럼, hashCode()를 name을 기준으로 오버라이딩했을 때, 객체의 실제 참조값(주소값)이 필요하다면 identityHashCode()를 사용해야 합니다.
  2. 객체의 원래 해시코드를 기반으로 특정한 로직을 구현할 때
  • 예를 들어, 객체를 원래 참조값 기준으로 관리하는 시스템을 구축할 때 identityHashCode()가 필요할 수 있습니다. 이는 캐시(Cache) 시스템이나 메모리 관리 시스템에서 유용하게 활용될 수 있습니다.

3.4 identityHashCode()와 == 연산자의 차이

📌 == vs identityHashCode() 비교 예제

Person p1 = new Person("홍길동");
Person p2 = new Person("홍길동");

System.out.println("p1 == p2: " + (p1 == p2)); // false (서로 다른 객체이므로 false)
System.out.println("p1.hashCode() == p2.hashCode(): " + (p1.hashCode() == p2.hashCode())); // true (name이 같아서)
System.out.println("System.identityHashCode(p1) == System.identityHashCode(p2): "
                    + (System.identityHashCode(p1) == System.identityHashCode(p2))); // false (서로 다른 객체)
  • p1 == p2는 서로 다른 객체이므로 false입니다.
  • p1.hashCode() == p2.hashCode()는 논리적으로 같은 값이므로 true입니다.
  • System.identityHashCode(p1) == System.identityHashCode(p2)는 각 객체의 참조값이 다르므로 false입니다.

4. 결론

  • equals()는 객체의 논리적 동등성을 비교하는 메서드이며, 기본적으로 참조값(주소)을 비교하지만 오버라이딩하여 특정 필드를 기준으로 비교할 수 있습니다.
  • hashCode()는 객체를 해시 기반 자료구조(HashSet, HashMap 등)에서 빠르게 찾을 수 있도록 정수형 해시값을 반환하는 메서드이며, 같은 값을 가진 객체는 같은 해시코드를 반환하도록 오버라이딩해야 합니다.
  • equals()를 오버라이딩하면 반드시 hashCode()도 함께 오버라이딩해야 합니다. 그렇지 않으면 해시 기반 컬렉션(HashSet, HashMap)에서 예상치 못한 동작이 발생할 수 있습니다.
  • hashCode()가 같아도 equals()가 반드시 true가 되는 것은 아닙니다. 이는 해시 충돌(Hash Collision) 때문에 발생할 수 있으며, 해시 기반 자료구조에서는 이를 고려하여 성능을 최적화해야 합니다.
  • identityHashCode()는 객체의 메모리 주소(참조값) 기반 해시코드를 반환하는 메서드입니다.
  • hashCode()를 오버라이딩하면 원래 객체의 주소 기반 해시값을 알 수 없지만, System.identityHashCode()를 사용하면 객체의 원래 해시코드를 얻을 수 있습니다.
  • 실무에서 equals()와 hashCode()를 오버라이딩할 때는 Objects.hash() 또는 java.util.HashCodeBuilder(Apache Commons)를 활용하여 안전하게 구현하는 것이 좋습니다.

'BackEnd > JAVA' 카테고리의 다른 글

[Java] String은 왜 불변(Immutable)할까? String Pool, intern(), 생성 방식까지 완벽 정리  (0) 2025.04.09
[JAVA] 익명 클래스 (Anonymous Class) 정리  (1) 2025.02.20
[JAVA] Lombok의 @UtilityClass 어노테이션 정리  (1) 2025.02.13
[JAVA] 얕은 복사 (Shallow Copy) VS 깊은 복사 (Deep Copy)  (2) 2025.02.06
[JAVA] 와일드카드 (Generic Wildcards) 정리  (2) 2025.02.05
'BackEnd/JAVA' 카테고리의 다른 글
  • [Java] String은 왜 불변(Immutable)할까? String Pool, intern(), 생성 방식까지 완벽 정리
  • [JAVA] 익명 클래스 (Anonymous Class) 정리
  • [JAVA] Lombok의 @UtilityClass 어노테이션 정리
  • [JAVA] 얕은 복사 (Shallow Copy) VS 깊은 복사 (Deep Copy)
개발자 동긔
개발자 동긔
배우고 느낀점들을 기록합니다. 열정 넘치는 백엔드 개발자로 남고싶습니다.
  • 개발자 동긔
    Donker Dev
    개발자 동긔
  • 전체
    오늘
    어제
    • Category (39)
      • BackEnd (23)
        • JAVA (15)
        • Spring & JPA (7)
      • Database (4)
      • Computer Science (2)
        • Network (0)
        • Security (0)
        • Web (1)
      • DevOps (6)
        • Docker (1)
        • Jenkins (0)
        • Monitoring (2)
        • CICD (1)
      • 트러블 슈팅 (3)
      • 성능 개선 (1)
      • Project (0)
  • 인기 글

  • 태그

    restful api
    restful api 설계
    @RequestBody
    master/slave db 이중화 처리
    SSH
    와일드카드
    CICD
    Jenkins
    spring boot
    인터페이스
    docker
    Database
    spring cloud msa
    JPA
    docker compose
    Spring
    java
    mysql master/slave replication
    nginx
    interface
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
개발자 동긔
[JAVA] equals()와 hashCode() 완벽 정리
상단으로

티스토리툴바