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 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- HashMap의 작동 원리
- equals/hashCode 계약
- hashCode 구현
- 트리화와 성능