11. equals를 재정의하려거든 hashCode도 재정의하라
11. equals를 재정의하려거든 hashCode도 재정의하라
🔑 핵심 내용
✔ equals를 재정의한 클래스는 반드시 hashCode도 재정의해야 한다 그렇지 않으면 HashMap, HashSet 같은 컬렉션에서 문제가 생긴다 ✔ 좋은 Hashcode 만들기
hashCode란 무엇일까?
hashCode는 객체를 구분하는 정수값(숫자)이다
이 숫자는 해시 기반 자료구조에서 객체를 빨리 찾는 데 사용한다
hashCode 규약
실행 중에는 같은 객체의 hashCode() 값이 변하면 안 된다
equals로 같다고 판단된 두 객체는 같은 hashCode 값을 가져야 한다
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 만드는 법
int 변수 result를 선언하고 첫 번째 필드의 해시코드로 초기화
나머지 필드의 해시코드를 계산해서 result 값에 더함
result = 31 * result + c // c는 필드의 해시코드
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을 곱할까?
31은 소수(Prime Number)라서 해시 충돌을 줄일 수 있다
31은 홀수라서 오버플로우 발생 시 정보 손실을 막을 수 있다
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