자바에서 객체의 동등성 검사는 매우 중요한 개념이며, 이를 위해 equals()
와 hashCode()
메서드를 올바르게 이해하고 구현하는 것이 필수적입니다.
equals()
: 두 객체가 논리적으로 같은지 비교하는 메서드hashCode()
: 객체를 빠르게 찾을 수 있도록 해시값(정수)를 반환하는 메서드
이 글에서는 equals()
와 hashCode()
의 개념, 원칙, 구현 방법, 컬렉션과의 관계, 그리고 실무에서의 활용까지 완벽하게 정리해보겠습니다.
1. eqauls()와 hashCode()란?
1.1 equals()란?
equals()
는 두 객체가 논리적으로 같은지 판단하는 메서드입니다.
Object
클래스의 기본equals()
는==
연산자와 동일하게 참조(주소) 비교를 수행합니다.- 따라서
equals()
를 오버라이딩하지 않으면, 동일한 값을 가진 객체라도 서로 다르다고 판단됩니다.
📌 equals()
기본 구현 (참조 비교)
class Person {
String name;
public Person(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("홍길동");
Person p2 = new Person("홍길동");
System.out.println(p1 == p2); // false (주소 비교)
System.out.println(p1.equals(p2)); // false (기본 equals()는 주소 비교)
}
}
위 코드에서 p1.equals(p2)
가 false
를 반환하는 이유는, Object
클래스의 기본 equals()
가 메모리 주소를 비교하기 때문입니다.
📌 equals()
오버라이딩 (논리적 비교)
객체가 동일한 값을 가질 경우 true
를 반환하도록 equals()
를 재정의할 수 있습니다.
class Person {
String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return name.equals(person.name);
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("홍길동");
Person p2 = new Person("홍길동");
System.out.println(p1.equals(p2)); // true (이제 논리적으로 동일하다고 판단)
}
}
1.2 hashCode()란?
hashCode()
는 객체를 빠르게 검색할 수 있도록 정수 형태의 해시값을 반환하는 메서드입니다.
- 기본적으로
Object
클래스의hashCode()
는 메모리 주소 기반 해시값을 반환합니다. hashCode()
를 적절히 오버라이딩하면, 같은 값을 가진 객체가 동일한 해시코드를 갖도록 만들 수 있습니다.
📌 hashCode()
기본 동작 예시
public class Main {
public static void main(String[] args) {
Person p1 = new Person("홍길동");
Person p2 = new Person("홍길동");
System.out.println(p1.hashCode()); // 예: 12345678
System.out.println(p2.hashCode()); // 예: 87654321 (값은 같지만 해시코드는 다름)
}
}
위 코드에서 p1
과 p2
는 같은 값을 가지지만, 해시코드는 다를 수 있습니다.
이를 해결하려면 hashCode()
를 적절히 재정의해야 합니다.
📌 hashCode()
오버라이딩
@Override
public int hashCode() {
return Objects.hash(name); // 필드를 기준으로 해시코드 생성
}
이제 name
이 동일하면 같은 해시코드를 반환하여, 해시 기반 컬렉션에서 정상적으로 동작할 수 있습니다.
2. equals()와 hashCode()의 관계
equals()
와 hashCode()
는 객체의 동등성을 검사할 때 반드시 함께 고려해야 하는 메서드입니다.
특히, 해시 기반 컬렉션(HashSet
, HashMap
, HashTable
)을 사용할 때 필수적으로 오버라이딩해야 합니다.
2.1 equals()와 hashCode() 구현 시 반드시 지켜야 할 규칙
1) equals()가 true이면 hashCode()도 같아야 한다.
- 논리적으로 동일한 객체라면(
equals()
가true
), 같은 해시코드를 반환해야 합니다. - 그렇지 않으면
HashSet
,HashMap
같은 컬렉션에서 제대로 동작하지 않습니다.
📌 hashCode()
를 재정의하지 않은 경우 (잘못된 코드)
import java.util.HashSet;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
}
public class Main {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
Person p1 = new Person("홍길동", 30);
Person p2 = new Person("홍길동", 30);
set.add(p1);
set.add(p2);
System.out.println(set.size()); // 예상: 1, 실제: 2 (중복 저장됨)
}
}
p1.equals(p2) == true
지만,hashCode()
를 오버라이딩하지 않았기 때문에 다른 해시코드를 가지게 됩니다.- 따라서 같은 객체임에도 HashSet에 중복 저장됩니다.
📌 hashCode()
를 올바르게 오버라이딩한 경우 (올바른 코드)
@Override
public int hashCode() {
return Objects.hash(name, age); // 같은 필드를 기준으로 해시코드 생성
}
- 이제
equals()
가true
인 경우,hashCode()
도 동일한 값을 반환하여 중복 저장이 방지됩니다.
2) hashCode()가 같다고 해서 equals()가 true를 보장하지 않는다.
- 서로 다른 객체라도 해시코드 값이 같을 수 있습니다. 이를 해시 충돌(Hash Collision) 이라고 합니다.
- 이는 자연스러운 현상이지만, 너무 자주 발생하면 해시 기반 컬렉션의 성능이 저하됩니다.
📌 잘못된 예제 (해시 충돌 발생)
class Car {
private String model;
public Car(String model) {
this.model = model;
}
@Override
public int hashCode() {
return 42; // 모든 객체가 같은 해시코드를 반환 (심각한 문제!)
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Car car = (Car) obj;
return model.equals(car.model);
}
}
public class Main {
public static void main(String[] args) {
Car c1 = new Car("Tesla");
Car c2 = new Car("BMW");
System.out.println(c1.hashCode()); // 42
System.out.println(c2.hashCode()); // 42 (같은 해시코드)
System.out.println(c1.equals(c2)); // false (다른 객체지만 같은 해시코드)
}
}
c1
과c2
는 다른 모델이지만,hashCode()
가 항상42
를 반환하여 같은 해시코드를 가집니다.- 해시 기반 컬렉션에서 성능이 급격히 저하될 수 있습니다.
📌 해결 방법 (해시 충돌 방지)
@Override
public int hashCode() {
return Objects.hash(model);
}
- 이제 다른 값은 서로 다른 해시코드를 가지게 되어 해시 충돌 문제 해결!
3) 객체가 변경되지 않는 한, hashCode()는 항상 동일한 값을 반환해야 한다.
- 객체를
HashSet
,HashMap
에 넣고 나서 객체의 필드 값을 변경하면 안 됩니다. - 필드가 변경되면
hashCode()
값이 바뀌어 컬렉션에서 객체를 찾지 못하는 문제 발생합니다.
📌 잘못된 예제 (해시코드가 변경되는 경우)
import java.util.HashSet;
class Employee {
private String name;
public Employee(String name) {
this.name = name;
}
@Override
public int hashCode() {
return name.hashCode();
}
public void setName(String newName) {
this.name = newName;
}
}
public class Main {
public static void main(String[] args) {
HashSet<Employee> set = new HashSet<>();
Employee e = new Employee("John");
set.add(e);
System.out.println(set.contains(e)); // true
e.setName("Mike"); // 객체의 필드 값 변경!
System.out.println(set.contains(e)); // false (예상: true)
}
}
e
의name
이 변경되면서hashCode()
값도 변경되었습니다.- 결과적으로 HashSet에서 객체를 찾지 못하는 문제가 발생합니다.
📌 해결 방법 (가변 필드 변경 금지)
- 해시코드에 사용되는 필드는 변경하지 않는 것이 원칙입니다.
- 만약 변경해야 한다면, 컬렉션에서 제거 후 다시 추가해야 합니다.
4) equals() 필드를 비교하는 경우, hashCode()도 동일한 필드를 기반으로 생성해야 한다.
equals()
가 비교하는 필드가hashCode()
에도 포함되어야 한다.
📌 잘못된 예제 (equals()
와 hashCode()
가 다른 기준 사용)
class Student {
private String name;
private int id; // 학번
public Student(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Student student = (Student) obj;
return name.equals(student.name); // name만 비교
}
@Override
public int hashCode() {
return id; // id만 사용 (불일치 문제 발생)
}
}
equals()
는name
만 비교하는데,hashCode()
는id
를 기반으로 생성됩니다.- 동일한
name
을 가진 객체라도 서로 다른 해시코드를 가질 가능성이 있습니다.
📌 해결 방법
@Override
public int hashCode() {
return Objects.hash(name); // `equals()`가 비교하는 필드와 동일한 필드를 사용
}
equals()
와hashCode()
가 동일한 기준을 사용하여 일관성 유지합니다.
2.2 equals()와 hashCode()의 동작 순서
equals()
와 hashCode()
의 동작과정은 다음과 같습니다.
hashCode()
를 먼저 호출하여 해시값을 비교합니다.- 만약 해시값이 다르면, 두 객체는 다른 객체로 판단됩니다.
- 만약 해시값이 같으면, 다음 단계로 진행됩니다.
equals()
를 호출하여 필드 값을 비교합니다.- 만약
equals()
가 true를 반환하면, 두 객체는 동등 객체로 판단됩니다. - 만약
equals()
가 false를 반환하면, 두 객체는 서로 다른 객체로 판단됩니다.
- 만약
즉, hashCode()
가 다르면 equals()
는 비교할 필요도 없이 false를 반환하지만, hashCode()
가 같아도 equals()
가 반드시 true가 되는 것은 아닙니다.
3. identityHashCode()란?
자바에서 hashCode()
는 원래 객체의 주소값을 기반으로 해시값을 반환하는 메서드입니다. 그러나, hashCode()
를 오버라이딩하여 객체의 필드를 기준으로 해시값을 반환하면, 객체의 원래 주소 기반 해시값을 얻기 어려워질 수 있습니다.
이 문제를 해결하기 위해 자바는 System.identityHashCode(Object obj)
를 제공합니다.
이 메서드는 객체의 메모리 주소(참조값)를 기반으로 한 해시코드를 반환합니다.
3.1 identityHashCode() vs hashCode() 차이점
- 객체의 논리적 동등성을 판단할 때는
hashCode()
를 오버라이딩하여 사용 - 객체의 원래 메모리 주소 기반 해시값이 필요할 때는
System.identityHashCode()
를 사용
3.2 identityHashCode()의 동작 방식
아래 예제에서 Person
클래스는 hashCode()
를 오버라이딩하여 name
필드를 기준으로 해시값을 반환하도록 구현했습니다.
하지만, System.identityHashCode()
를 사용하면 객체의 원래 해시코드(메모리 주소 기반)가 반환되는 것을 확인할 수 있습니다.
📌 hashCode()
와 identityHashCode()
비교 예제
import java.util.Objects;
class Person {
String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return Objects.equals(this.name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name); // name을 기반으로 해시코드 생성
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("홍길동");
Person p2 = new Person("홍길동");
// equals()와 hashCode()를 오버라이딩했기에, 두 객체의 필드 값이 같다면 동일한 해시코드 반환
System.out.println("p1.hashCode(): " + p1.hashCode()); // 예: 54150093
System.out.println("p2.hashCode(): " + p2.hashCode()); // 예: 54150093 (같음)
// 하지만, 실제 객체의 주소값을 알고 싶다면 identityHashCode()를 사용해야 함
System.out.println("System.identityHashCode(p1): " + System.identityHashCode(p1)); // 예: 622488023
System.out.println("System.identityHashCode(p2): " + System.identityHashCode(p2)); // 예: 414493378 (다름)
}
}
p1.hashCode()
와p2.hashCode()
가 같은 이유는name
필드를 기준으로hashCode()
를 오버라이딩했기 때문입니다.- 그러나,
System.identityHashCode(p1)
와System.identityHashCode(p2)
는 각각 다른 값을 반환합니다. - 이는
identityHashCode()
가 객체의 참조값(메모리 주소 기반 해시값)을 반환하기 때문입니다.
3.3 identityHashCode()가 필요한 경우
hashCode()
를 오버라이딩했지만 원래 객체의 참조값이 필요한 경우- 위의 예제처럼,
hashCode()
를name
을 기준으로 오버라이딩했을 때, 객체의 실제 참조값(주소값)이 필요하다면identityHashCode()
를 사용해야 합니다.
- 위의 예제처럼,
- 객체의 원래 해시코드를 기반으로 특정한 로직을 구현할 때
- 예를 들어, 객체를 원래 참조값 기준으로 관리하는 시스템을 구축할 때
identityHashCode()
가 필요할 수 있습니다. 이는 캐시(Cache) 시스템이나 메모리 관리 시스템에서 유용하게 활용될 수 있습니다.
3.4 identityHashCode()와 == 연산자의 차이
📌 ==
vs identityHashCode()
비교 예제
Person p1 = new Person("홍길동");
Person p2 = new Person("홍길동");
System.out.println("p1 == p2: " + (p1 == p2)); // false (서로 다른 객체이므로 false)
System.out.println("p1.hashCode() == p2.hashCode(): " + (p1.hashCode() == p2.hashCode())); // true (name이 같아서)
System.out.println("System.identityHashCode(p1) == System.identityHashCode(p2): "
+ (System.identityHashCode(p1) == System.identityHashCode(p2))); // false (서로 다른 객체)
p1 == p2
는 서로 다른 객체이므로false
입니다.p1.hashCode() == p2.hashCode()
는 논리적으로 같은 값이므로true
입니다.System.identityHashCode(p1) == System.identityHashCode(p2)
는 각 객체의 참조값이 다르므로false
입니다.
4. 결론
equals()
는 객체의 논리적 동등성을 비교하는 메서드이며, 기본적으로 참조값(주소)을 비교하지만 오버라이딩하여 특정 필드를 기준으로 비교할 수 있습니다.hashCode()
는 객체를 해시 기반 자료구조(HashSet
,HashMap
등)에서 빠르게 찾을 수 있도록 정수형 해시값을 반환하는 메서드이며, 같은 값을 가진 객체는 같은 해시코드를 반환하도록 오버라이딩해야 합니다.equals()
를 오버라이딩하면 반드시hashCode()
도 함께 오버라이딩해야 합니다. 그렇지 않으면 해시 기반 컬렉션(HashSet
,HashMap
)에서 예상치 못한 동작이 발생할 수 있습니다.hashCode()
가 같아도equals()
가 반드시true
가 되는 것은 아닙니다. 이는 해시 충돌(Hash Collision) 때문에 발생할 수 있으며, 해시 기반 자료구조에서는 이를 고려하여 성능을 최적화해야 합니다.identityHashCode()
는 객체의 메모리 주소(참조값) 기반 해시코드를 반환하는 메서드입니다.hashCode()
를 오버라이딩하면 원래 객체의 주소 기반 해시값을 알 수 없지만,System.identityHashCode()
를 사용하면 객체의 원래 해시코드를 얻을 수 있습니다.- 실무에서
equals()
와hashCode()
를 오버라이딩할 때는Objects.hash()
또는java.util.HashCodeBuilder
(Apache Commons)를 활용하여 안전하게 구현하는 것이 좋습니다.
'BackEnd > JAVA' 카테고리의 다른 글
[JAVA] 익명 클래스 (Anonymous Class) 정리 (1) | 2025.02.20 |
---|---|
[JAVA] Lombok의 @UtilityClass 어노테이션 정리 (1) | 2025.02.13 |
[JAVA] 얕은 복사 (Shallow Copy) VS 깊은 복사 (Deep Copy) (2) | 2025.02.06 |
[JAVA] 와일드카드 (Generic Wildcards) 정리 (1) | 2025.02.05 |
[JAVA] 제네릭 (Generic) 정리 (1) | 2025.01.26 |