hashCode 구현
올바른 해시 함수를 작성합니다
hashCode 구현은(는) CoddyKit의 무료 Java Academy 강의입니다. 이것은 4개 중 3번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Java Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Java Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
좋은 hashCode의 목표
좋은 hashCode()는 다음 조건을 만족해야 합니다:
- 동등한 객체에 대해 같은 값을 반환합니다(계약).
- 서로 같지 않은 객체를 서로 다른 많은 값에 분산합니다.
- 계산 비용이 낮아야 합니다.
항상 상수를 반환하는 형편없는 hashCode도 계약은 충족하지만 맵을 느린 연결 리스트로 바꿉니다.
public class Main {
public static void main(String[] args) {
// Legal but terrible: every object collides
System.out.println("constant hashCode is legal but kills performance");
}
}일반적인 경우의 Objects.hash
가장 간단하면서 올바른 방법은 Objects.hash(field1, field2, ...)입니다.
널 값을 처리하고 표준 알고리즘으로 필드를 결합합니다. equals에서 비교하는 같은 필드를 사용하세요.
import java.util.Objects;
public class Main {
static class User {
final String name; final int age;
User(String name, int age) { this.name = name; this.age = age; }
@Override public int hashCode() { return Objects.hash(name, age); }
}
public static void main(String[] args) {
User a = new User("Ada", 36);
User b = new User("Ada", 36);
System.out.println(a.hashCode() == b.hashCode());
}
}전통적인 31 곱셈자
직접 작성한 해시에서는 누적 결과에 31을 곱한 다음 각 필드의 해시를 더하는 표준 패턴을 사용합니다.
31은 홀수 소수이고 31 * x는 (x << 5) - x와 같으므로 JVM이 이를 최적화할 수 있습니다.
public class Main {
static class User {
final String name; final int age;
User(String name, int age) { this.name = name; this.age = age; }
@Override public int hashCode() {
int result = 17;
result = 31 * result + (name == null ? 0 : name.hashCode());
result = 31 * result + age;
return result;
}
}
public static void main(String[] args) {
System.out.println(new User("Ada", 36).hashCode());
}
}기본 타입의 해시 계산
각 기본 타입에는 권장되는 해시 계산 방법이 있습니다:
int: 값 자체를 사용합니다.long:(int)(value ^ (value >>> 32))를 사용합니다.boolean: 1 또는 0을 사용합니다.double:Double.hashCode(value)를 사용합니다.
public class Main {
public static void main(String[] args) {
long id = 4_000_000_000L;
int longHash = (int) (id ^ (id >>> 32));
System.out.println("long hash: " + longHash);
System.out.println("double hash: " + Double.hashCode(3.14));
System.out.println("bool hash: " + Boolean.hashCode(true));
}
}배열의 해시 계산
배열에서 hashCode()를 직접 호출하지 마세요. 내용이 아니라 동일성을 사용하기 때문입니다.
일차원 배열에는 Arrays.hashCode(arr)를 사용하고, 중첩 배열에는 Arrays.deepHashCode(arr)를 사용하세요.
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
System.out.println("identity equal: " + (a.hashCode() == b.hashCode()));
System.out.println("content equal: " + (Arrays.hashCode(a) == Arrays.hashCode(b)));
}
}equals와 hashCode를 일치시키기
hashCode()에서 사용하는 필드는 equals()에서 사용하는 필드의 부분집합이어야 합니다(이상적으로는 정확히 같아야 합니다).
equals가 hashCode보다 더 많은 필드를 비교하면 동등한 두 객체가 여전히 같은 해시를 공유하므로 허용됩니다. 그러나 hashCode가 equals에서 무시하는 필드를 사용하면 계약을 위반합니다.
import java.util.Objects;
public class Main {
static class Coord {
final int x, y;
Coord(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
return o instanceof Coord c && c.x == x && c.y == y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
public static void main(String[] args) {
Coord a = new Coord(3, 4), b = new Coord(3, 4);
System.out.println(a.equals(b) && a.hashCode() == b.hashCode());
}
}해시 캐싱
해시 계산 비용이 큰 불변 객체라면 결과를 필드에 캐시할 수 있습니다.
String이 내부적으로 정확히 이렇게 합니다. 객체가 진정으로 불변인 경우에만 사용하여 캐시된 값이 절대 오래된 상태가 되지 않도록 하세요.
import java.util.Objects;
public class Main {
static final class Key {
final String a, b;
private int hash; // 0 until computed
Key(String a, String b) { this.a = a; this.b = b; }
@Override public int hashCode() {
int h = hash;
if (h == 0) { h = Objects.hash(a, b); hash = h; }
return h;
}
}
public static void main(String[] args) {
Key k = new Key("x", "y");
System.out.println(k.hashCode());
System.out.println(k.hashCode());
}
}분포가 중요합니다
잘 분산된 hashCode는 키를 버킷 전체에 고르게 분배합니다. 객체 묶음에 서로 다른 hash 코드가 몇 개인지 세어 보겠습니다.
서로 다른 값이 많을수록 충돌은 줄어들고 맵은 빨라집니다.
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Main {
record Pair(int a, int b) {}
public static void main(String[] args) {
Set<Integer> hashes = new HashSet<>();
for (int i = 0; i < 100; i++) {
hashes.add(Objects.hash(i, i * 7));
}
System.out.println("distinct hashes: " + hashes.size());
}
}나쁜 분포의 예
필드를 곱하지 않고 더하기만 하면 충돌이 발생합니다: (1,2)와 (2,1)은 모두 3으로 해시됩니다.
31 곱셈자는 순서가 중요해지므로 이 대칭성을 깨뜨립니다.
public class Main {
static int badHash(int a, int b) { return a + b; }
static int goodHash(int a, int b) { return 31 * a + b; }
public static void main(String[] args) {
System.out.println("bad (1,2): " + badHash(1, 2) + ", (2,1): " + badHash(2, 1));
System.out.println("good (1,2): " + goodHash(1, 2) + ", (2,1): " + goodHash(2, 1));
}
}값 타입에는 레코드를 우선 사용하기
순수한 데이터 전달자에는 record가 올바르고 분포가 좋은 hashCode를 자동으로 생성합니다.
사용자 지정 의미가 필요하거나 레코드를 사용할 수 없는 경우에만 hashCode를 직접 작성하세요.
public class Main {
record Money(long cents, String currency) {}
public static void main(String[] args) {
Money a = new Money(1099, "USD");
Money b = new Money(1099, "USD");
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
}
}종합하기
완전한 값 클래스의 구성은 다음과 같습니다: 불변 필드, 동일한 필드로 구현한 equals와 hashCode, 깔끔한 toString입니다.
import java.util.Objects;
public class Main {
static final class Version {
final int major, minor, patch;
Version(int major, int minor, int patch) {
this.major = major; this.minor = minor; this.patch = patch;
}
@Override public boolean equals(Object o) {
return o instanceof Version v && v.major == major && v.minor == minor && v.patch == patch;
}
@Override public int hashCode() { return Objects.hash(major, minor, patch); }
@Override public String toString() { return major + "." + minor + "." + patch; }
}
public static void main(String[] args) {
Version v = new Version(2, 1, 0);
System.out.println(v + " hash=" + v.hashCode());
}
}빠른 확인
hashCode 작성 실력을 확인해 보세요.
복습
hashCode를 올바르게 구현하는 방법을 학습했습니다:
- 일반적인 경우에는
Objects.hash(...)를 사용합니다. - 직접 해시를 작성할 때는 31 곱셈자 패턴을 사용합니다.
- 기본 방식이 아니라
Arrays.hashCode로 배열을 해시합니다. - hashCode에 사용하는 필드를 equals와 일치시키고 레코드를 우선 사용합니다.
다음에는 Java 8+에서 과부하된 버킷을 어떻게 트리화하는지 살펴봅니다.
import java.util.Objects;
public class Main {
public static void main(String[] args) {
System.out.println("hashCode recap done: " + Objects.hash("done"));
}
}자주 묻는 질문
“hashCode 구현” 강의는 무료인가요?
네 — “hashCode 구현” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Java Academy 강의 전체를 잠금 해제할 수 있습니다. Java Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“hashCode 구현”에서 뭘 배우나요?
올바른 해시 함수를 작성합니다 브라우저에서 직접 실행하는 실습 코드로 Java Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Java Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Java Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 3번째 강의입니다.
“hashCode 구현” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Java Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Java Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- HashMap의 작동 원리
- equals/hashCode 계약
- hashCode 구현
- 트리화와 성능