0Pricing
Java Academy · 강의

equals/hashCode 계약

두 메서드가 일치해야 하는 이유를 알아봅니다

equals/hashCode 계약은(는) CoddyKit의 무료 Java Academy 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Java Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Java Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

두 메서드, 하나의 계약

모든 Java 객체는 equals()와 hashCode()를 Object에서 상속합니다.

하나를 재정의하면 거의 항상 다른 하나도 재정의해야 합니다. 이 둘은 해시 기반 컬렉션이 의존하는 긴밀한 계약을 이룹니다.

public class Main {
    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();
        System.out.println(a.equals(b));
        System.out.println(a.hashCode() == b.hashCode());
    }
}

핵심 규칙

계약 내용은 다음과 같습니다:

  • a.equals(b)가 참이면 a.hashCode()는 b.hashCode()와 같아야 합니다.
  • 해시 코드가 다르면 객체는 반드시 서로 같지 않습니다.

동등한 객체는 같은 해시 코드를 가져야 합니다. 그 역은 요구되지 않습니다.

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        System.out.println(s1.equals(s2));
        System.out.println(s1.hashCode() == s2.hashCode());
    }
}

서로 같지 않은 객체는 같은 해시를 가질 수 있습니다

서로 같지 않은 두 객체가 같은 hash 코드를 가져도 허용됩니다. 이는 단순한 충돌이며, 맵은 equals()로 이를 처리합니다.

따라서 유일하게 금지되는 상황은 동등한 객체의 hash 코드가 서로 다른 경우입니다.

public class Main {
    public static void main(String[] args) {
        // Equal value, unequal objects, same hash is fine
        System.out.println("FB".hashCode() == "Ea".hashCode());
        System.out.println("FB".equals("Ea"));
    }
}

equals만 재정의하는 클래스

여기 깨진 클래스가 있습니다. equals()는 재정의하지만 hashCode()는 재정의하지 않습니다.

논리적으로 같은 두 점은 이제 객체에서 상속된 서로 다른 hashCode를 가지게 되어 계약을 위반합니다.

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
        // BUG: no hashCode override
    }
    public static void main(String[] args) {
        Point a = new Point(1, 2);
        Point b = new Point(1, 2);
        System.out.println("equals: " + a.equals(b));
        System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
    }
}

HashSet의 버그

깨진 클래스는 HashSet에서 실패합니다. 집합은 잘못된 hash로 선택된 버킷을 확인하므로 동등한 요소를 찾지 못합니다.

거부되었어야 할 중복이 남게 됩니다.

import java.util.HashSet;
import java.util.Set;

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

수정하기

두 메서드 모두를 재정의하고 동일한 필드에서 결과를 도출합니다. 그러면 동등한 객체는 같은 hash를 공유하고 같은 버킷에 들어갑니다.

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

equals는 반사적이어야 합니다

올바른 equals()는 네 가지 속성을 지킵니다. 첫 번째는 반사적이라는 것입니다: x.equals(x)는 반드시 참이어야 합니다.

당연하게 들리지만, 같은 참조인 경우를 처리하는 것을 잊으면 사용자 지정 로직으로 인해 이 규칙이 깨질 수 있습니다.

public class Main {
    public static void main(String[] args) {
        String x = "java";
        System.out.println(x.equals(x));
    }
}

대칭성과 추이성

두 가지 규칙이 더 있습니다:

  • 대칭적: a.equals(b)이면 b.equals(a)입니다.
  • 추이적: a.equals(b)이고 b.equals(c)이면 a.equals(c)입니다.

클래스 계층 구조에서 타입을 섞으면 대칭성이 깨지는 경우가 많으므로 클래스를 신중하게 비교하세요.

public class Main {
    public static void main(String[] args) {
        String a = "x", b = "x", c = "x";
        boolean symmetric = a.equals(b) == b.equals(a);
        boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
        System.out.println("symmetric: " + symmetric);
        System.out.println("transitive: " + transitive);
    }
}

일관성

네 번째 규칙은 일관적이어야 한다는 것입니다. 객체가 변경되지 않는 한 반복 호출은 같은 결과를 반환해야 합니다.

따라서 equals와 hashCode는 불변 필드를 기반으로 작성해야 합니다. 삽입한 후 hashCode에 사용되는 필드를 변경하면 맵이 손상됩니다.

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        // Immutable record gives consistent equals/hashCode automatically
        record Id(int value) {}
        Id id = new Id(7);
        System.out.println(Objects.equals(id, new Id(7)));
        System.out.println(id.hashCode() == new Id(7).hashCode());
    }
}

레코드는 이를 자동으로 처리합니다

Java record는 모든 구성 요소를 바탕으로 equals()와 hashCode()를 자동으로 생성합니다.

값과 유사한 데이터에는 레코드를 우선 사용하세요. 계약이 자동으로 충족되고 서로 일관되게 유지됩니다.

import java.util.HashSet;
import java.util.Set;

public class Main {
    record Point(int x, int y) {}
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

변경 가능한 키의 함정

맵에 객체를 넣은 후 hashCode에 사용되는 필드를 변경하면 더 이상 객체를 찾을 수 없습니다.

맵은 이전 버킷을 찾지만 객체는 이제 새 버킷으로 해시됩니다. 항목은 잃어버린 유령이 됩니다.

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Main {
    static class Box {
        int id;
        Box(int id) { this.id = id; }
        @Override public boolean equals(Object o) {
            return o instanceof Box b && b.id == id;
        }
        @Override public int hashCode() { return Objects.hash(id); }
    }
    public static void main(String[] args) {
        Map<Box, String> m = new HashMap<>();
        Box key = new Box(1);
        m.put(key, "value");
        key.id = 99; // mutated after insertion
        System.out.println(m.get(key));
    }
}

빠른 확인

계약을 얼마나 잘 이해했는지 확인해 보세요.

복습

equals/hashCode 계약을 학습했습니다:

  • 동등한 객체는 같은 해시 코드를 가져야 합니다.
  • equals는 반사적이고, 대칭적이며, 추이적이고, 일관적이어야 합니다.
  • 두 메서드 모두 같은 불변 필드를 기반으로 작성해야 합니다.
  • 레코드는 올바른 구현을 자동으로 생성합니다.

다음에는 직접 올바른 hashCode를 작성합니다.

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
    }
}

자주 묻는 질문

“equals/hashCode 계약” 강의는 무료인가요?

네 — “equals/hashCode 계약” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Java Academy 강의 전체를 잠금 해제할 수 있습니다. Java Academy 강의에는 총 4개의 강의가 포함되어 있습니다.

“equals/hashCode 계약”에서 뭘 배우나요?

두 메서드가 일치해야 하는 이유를 알아봅니다 브라우저에서 직접 실행하는 실습 코드로 Java Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Java Academy을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Java Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“equals/hashCode 계약” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Java Academy 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Java Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

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