0Pricing
Java Academy · 강의

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 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. HashMap의 작동 원리
  2. equals/hashCode 계약
  3. hashCode 구현
  4. 트리화와 성능
← Java Academy(으)로 돌아가기