11. equals를 재정의하려거든 hashCode도 재정의하라

11. equals를 재정의하려거든 hashCode도 재정의하라

🔑 핵심 내용

✔ equals를 재정의한 클래스는 반드시 hashCode도 재정의해야 한다 그렇지 않으면 HashMap, HashSet 같은 컬렉션에서 문제가 생긴다 ✔ 좋은 Hashcode 만들기


hashCode란 무엇일까?

  • hashCode는 객체를 구분하는 정수값(숫자)이다

  • 이 숫자는 해시 기반 자료구조에서 객체를 빨리 찾는 데 사용한다

hashCode 규약

  1. 실행 중에는 같은 객체의 hashCode() 값이 변하면 안 된다

  2. equals로 같다고 판단된 두 객체는 같은 hashCode 값을 가져야 한다

  3. equals로 다르다고 판단된 두 객체의 hashCode는 다를 수도, 같을 수도 있다

🚫 나쁜 예시 : equals만 재정의하고 hashCode는 재정의하지 않은 경우

public class Student {
    private String name;
    private int grade;

    // 생성자
    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    // equals만 재정의 (이름과 학년이 같으면 같은 학생)
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return grade == student.grade &&
               Objects.equals(name, student.name);
    }

    // hashCode는 재정의하지 않음!
}

public static void main(String[] args) {
    Map<Student, String> studentMap = new HashMap<>();

    // 첫 번째 민수 객체 생성
    Student student1 = new Student("민수", 3);
    // 두 번째 민수 객체 생성 (equals로는 같은 학생)
    Student student2 = new Student("민수", 3);

    // 첫 번째 민수를 맵에 저장
    studentMap.put(student1, "출석");

    // 두 번째 민수의 정보를 조회
    String status = studentMap.get(student2);

    System.out.println("민수의 상태: " + status); // null이 출력됨
}

✅ 좋은 예시 : equals와 hashCode 둘다 재정의 한 경우

public class Student {
    private String name;
    private int grade;

    // 생성자
    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return grade == student.grade &&
               Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + grade;
        return result;
    }
}

// 실제 사용 예시
public static void main(String[] args) {
    Map<Student, String> studentMap = new HashMap<>();

    // 첫 번째 민수 객체 생성
    Student student1 = new Student("민수", 3);
    // 두 번째 민수 객체 생성 (equals로는 같은 학생)
    Student student2 = new Student("민수", 3);

    // 첫 번째 민수를 맵에 저장
    studentMap.put(student1, "출석");

    // hashCode를 재정의했으므로 두 번째 민수의 정보를 제대로 조회할 수 있음
    String status = studentMap.get(student2);

    System.out.println("민수의 상태: " + status); // "출석"이 출력됨!

    // HashSet에서도 제대로 동작
    Set<Student> studentSet = new HashSet<>();
    studentSet.add(student1);

    // 이미 같은 학생이 있어서 false 반환
    boolean added = studentSet.add(student2);
    System.out.println("추가 여부: " + added); // false 출력
    System.out.println("Set 크기: " + studentSet.size()); // 1 출력
}

좋은 hashCode 만드는 법

  1. int 변수 result를 선언하고 첫 번째 필드의 해시코드로 초기화

  2. 나머지 필드의 해시코드를 계산해서 result 값에 더함

    result = 31 * result + c   // c는 필드의 해시코드
  3. result 값을 리턴

hashCode() 작성 예시

class Person {
    String name;
    int age;

    @Override
    public int hashCode() {
        int result = name.hashCode(); // 1️⃣ 첫 번째 핵심 필드 사용
        result = 31 * result + age;   // 2️⃣ 31을 곱한 후 두 번째 필드 추가
        return result;                // 3️⃣ 최종 해시코드 반환
    }
}

hashCode() 작성 시 고려해야 할 사항

  • 숫자 타입 (int, long 등) → 그냥 사용

  • 문자열 (String) → .hashCode() 호출

  • 객체 → 해당 객체의 hashCode() 사용

  • 배열 → Arrays.hashCode() 사용

  • null 값 → 0 사용

왜 31을 곱할까?

  1. 31은 소수(Prime Number)라서 해시 충돌을 줄일 수 있다

  2. 31은 홀수라서 오버플로우 발생 시 정보 손실을 막을 수 있다

  3. 31 * x는 (x << 5) - x로 바꿀 수 있어 연산 속도가 빠르다

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

    x << 5 는 x _ 32 와 같음 (왼쪽으로 5비트 이동 = 2^5 = 32 곱하기) 즉, 31 _ x 를 32 * x - x 로 변경하면 빠른 연산이 가능!

더 쉬운 방법: Objects.hash 사용하기

  • Objects.hash 를 사용하면 편리하지만, 성능이 저하 된다는 단점이 있다

  • 성능이 중요한 애플리케이션이 아닌 일반적인 애플리케이션에서는 이 성능 차이가 크게 문제되지 않을 수 있다

@Override
public int hashCode() {
    return Objects.hash(name, grade);
}


🧩 어려웠던 점

  • hashCode()를 왜 재정의해야 하는지 처음에는 이해하기 어려웠음.

  • equals()와 hashCode()의 관계를 정확히 정리하는 것이 헷갈렸다

  • 자바가 자동으로 해결해주지 않아서 개발자가 직접 관리해야 하는 부분이라 처음에는 어려웠다

💭 느낀 점

  • hashCode()를 재정의하지 않으면, 같은 객체라도 컬렉션에서 찾지 못할 수 있다는 것에 놀랐다

  • 31을 곱하는 이유가 단순한 최적화가 아니라, 실제 성능에 영향을 미친다는 점이 흥미로웠다

Last updated