equals()
`equals()` 메서드는 어떤 다른 객체가 이 객체와 동등한지를 나타낸다.
`null`이 아닌 참조 값 `x`와 `y`에 대해 이 메서드는 `x`와 `y`가 같은 객체를 참조하는 경우(`x == y`가 `true`인 경우)에만 `true`를 반환한다.
이 메서드를 오버라이딩할 때는 일반적으로 `hashCode()` 메서드도 함께 오버라이딩해야 한다. 이는 `hashCode()` 메서드의 일반적인 계약을 유지하기 위함인데, 이 계약은 동등한 객체들은 동등한 해시 코드를 가져야 한다고 명시한다.
다음은 java8 references의 `equals()` 메서드 부분의 일부를 최대한 원문으로 번역해보았다.
기본 타입이 아닌 참조 타입의 객체를 비교할 때, `equals()` 메서드를 사용한다. 자바는 클래스, 인터페이스, 컬렉션은 참조 타입을 기반으로 설계되었고, 다형성을 활용하기 위해서도 참조 타입이 필요하다.
따라서 `equals`를 쓸 일이 많다. `equals`는 `hashCode()`와 일반 계약을 했다고 하는데, 이 계약은 무엇이며, `hashCode`는 무슨 일을 할까?
hashCode()
객체의 해시 코드를 반환한다. 이 메서드는 `HashMap`과 같은 해시 테이블의 이점을 위해 지원된다.
`hashCode`의 일반적인 계약은 다음과 같다:
1. Java 애플리케이션 실행 중에 같은 객체에 대해 여러 번 호출될 때, 객체의 `equals()` 비교에 사용되는 정보가 수정되지 않는 한, `hashCode()` 메서드는 일관되게 동일한 정수를 반환해야 한다.
2. 두 객체가 `equals()` 메서드에 따라 동등하다면, 두 객체 각각에 대해 `hashCode()` 메서드를 호출하면 동일한 정수 결과를 생성해야 한다.
3. 두 객체가 `equals()` 메서드에 따라 동등하지 않다면, 두 객체 각각에 대해 `hashCode()` 메서드를 호출할 때 서로 다른 정수 결과를 생성할 필요는 없다. 그러나, 프로그래머는 동등하지 않은 객체에 대해 서로 다른 정수 결과를 생성하는 것이 해시 테이블의 성능을 향상시킬 수 있다는 점을 알아야 한다.
`hashCode()`는 객체의 주소값을 해시값(객체를 구별할 수 있는 정수값)을 생성한다.
💡주소값을 그대로 쓰지 않고 해시값을 사용하는 이유는 뭘까?
64비트 OS를 사용한다면 64비트의 주소값은 매우 큰 수가 될 수 있다. 이 주소값을 직접 인덱스로 사용하면 매우 큰 배열이 필요하기 때문에 메모리 낭비가 크다.
`hashCode()`는 `int`타입의 값을 반환하기 때문에 64비트의 주소값을 32비트의 해시값으로 바꾸면 인덱스 연산도 빠르고, 메모리 공간도 절약할 수 있다.
64비트보다 32비트는 해시 충돌이 발생할 수도 있지만, 좋은 해시 함수는 충돌 가능성을 최소화하고 해시 충돌이 발생하더라도 체이닝, 오픈 어드레싱 등의 기법으로 처리할 수 있다.
equals()와 hashCode()는 함께 오버라이딩 해야 한다!
프로그래머는 객체의 필드값이 같다면 같은 객체로 정의하고자 할 때가 많다. 이를 위해 `equals()`를 오버라이딩하지만, `hashCode()`도 함께 오버라이딩하지 않으면 문제가 발생한다. 예를 들어, `equals()`만 오버라이딩하고 `hashCode()`는 오버라이딩하지 않은 클래스의 객체를 `HashMap`이나 `HashSet`에 사용하는 경우:
public class Person {
private String name;
private int age;
// 생성자, getter, setter 생략
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Person)) return false;
Person other = (Person) obj;
return age == other.age && Objects.equals(name, other.name);
}
// hashCode()는 오버라이딩하지 않음
// @Override
// public int hashCode() {
// return Objects.hash(name, age);
// }
}
Person p1 = new Person("홍길동", 20);
Person p2 = new Person("홍길동", 20);
System.out.println(p1.equals(p2)); // true (필드값이 같으므로)
Map<Person, String> map = new HashMap<>();
map.put(p1, "사람1");
System.out.println(map.get(p2)); // null이 출력됨 (예상과 다름!)
위 코드에서 `p1.equals(p2)`는 `true`를 반환하지만, `map.get(p2)`는 `null`을 반환한다.
이유는 `HashMap`이 객체를 저장하고 검색할 때:
- 먼저 객체의 `hashCode()`를 호출하여 해당 해시 버킷을 찾는다.
- 같은 버킷 내에서 `equals()`를 사용해 정확한 객체를 찾는다.(해시 충돌을 대비하기 위해)
`hashCode()`를 오버라이딩하지 않았기 때문에 `p1`과 `p2`는 서로 다른 해시 버킷에 저장되어, `p2`의 해시 코드로는 `p1`이 저장된 버킷을 찾을 수 없다.
따라서 `equals()`를 오버라이딩할 때는 반드시 `hashCode()`도 함께 오버라이딩하여, 동등한 객체가 같은 해시 코드를 반환하도록 해야 한다.
'Java' 카테고리의 다른 글
[Java] 동일성과 동등성에 관하여 (0) | 2025.03.17 |
---|---|
[Java] 얕은 복사와 깊은 복사 (3) | 2025.03.11 |
[Java] 일급 컬렉션이란? (1) | 2025.03.08 |
[Java] Checked Exception과 Unchecked Exception (1) | 2025.03.07 |