반응형
equals를 재정의하려거든 hashCode도 재정의하라
equals
재정의 후 hashCode
를 재정의해야 한다. 그 이유는 논리적으로 같은 객체는 같은 해시코드를 반환해야하기 때문이다.
Hash 함수를 사용하는 컬렉션을 사용하지 않으면 hashCode를 재정의하지 않더라도 문제가 발생하지 않는다. 하지만 애플리케이션 레벨에서 바라봤을 때 해시 컬렉션을 사용하지 않는다고 확신하기 어렵다. 따라서 hashCode도 같이 재정의 하는 것이 필요하다.
equals만 재정의 한 경우
import java.util.*;
public class Car {
private final String name;
public Car(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return Objects.equals(name, car.name);
}
}
class CarTest {
public static void main(String[] args) {
Car carA = new Car("sasca");
Car carB = new Car("sasca");
System.out.println(carA.equals(carB)); // true
List<Car> carList = new ArrayList<>();
carList.add(new Car("sasca"));
carList.add(new Car("sasca"));
System.out.println(carList.size()); // 2
Set<Car> carSet = new HashSet<>();
carSet.add(new Car("sasca"));
carSet.add(new Car("sasca"));
System.out.println(carSet.size()); //2 (문제 발생)
}
}
equals
만 재정의한 경우 두 객체는 논리적으로 같은 객체로 판단되며 equals는 true의 결과 값을 갖게 된다. 당연히List
타입의 컬렉션에 두 객체를 넣으면 정상 동작한다.Set
타입의 중복되지 않는 객체를 넣을 때 문제가 발생한다. 1이 나와야하지만, 예상과 다르게 2가 나온다. 그 이유는hash
값을 사용하는 컬렉션을 사용할 때 문제가 발생하기 때문이다.
hash Collection 동작 과정
- 컬렉션의
HashMap
,HashSet
,HashTable
은 객체가 논리적으로 같은지 비교할 때 다음과 같은 과정을 거친다.
System.out.println(carA.hashCode()); // 2109957412
System.out.println(carB.hashCode()); // 901506536
- 두 객체의 Object 클래스의 hashCode를 출력해보면 다르다는 것을 볼 수 있다. 즉, hashCode가 다르기 때문에 equals 비교를 하기 전에 다른 객체로 판단이 된다.
hashCode도 같이 재정의한 경우
@Override
public int hashCode() {
// return Objects.hash(name); JAVA 7
return name.hashCode();
}
System.out.println(carA.hashCode()); // 109208323
System.out.println(carB.hashCode()); // 109208323
Set<Car> carSet = new HashSet<>();
carSet.add(new Car("sasca"));
carSet.add(new Car("sasca"));
System.out.println(carSet.size()); // 1 정상 동작
hashCode
를 재정의 해줌으로 같은 해시 코드 값을 반환하게 되었다. 따라서 해시 컬렉션에서 equals를 비교할 수 있게 되고 동등 객체로 판단이 된다.- 자바 7버전 부터 사용 가능한
Objects.hash()
유틸리티 메서드, 롬복의@EqualsAndHashCode
를 통해 편리한 해싱을 적용할 수 있다.
HashCode 구현 과정
@Override
public int hashCode(){
return 31;
}
- 사실 다음과 같이 별도의 연산 과정 없이 정수를 리턴해도 재정의를 통해 보장할 수 있다. 하지만 동등 객체가 아닌 다른 객체에서도 같은 값을 반환하기 때문에 해시 테이블의 버킷이 연결리스트로 존재하여 성능이 O(1)인 해시테이블이 O(N)이 된다.
@Override
public int hashCode(){
return Objects.hash(name);
}
- 해당 방식은 위에서 설명한 자바 7에서 제공하는 유틸리티 메서드를 이용하는 방식이다. 정수를 리턴하는 것보다 효율적이지만 매번 계산하는 방식의 성능이 아쉽다.
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = 31 * name.hashCode();
// result = 31 * result + Short.hashCode(prefix);
hashCode = result;
}
return result;
}
- 지연 초기화 전략을 통해 캐싱하는 방식을 고려하면 최적의 성능을 보여줄 수 있을 것이다. 단, 필드를 지연 초기화하려면 스레드 세이프하도록 신경써야 한다.
hashCode
를 구현하는 것에 보편적인 방법은 없지만 대부분31
번을 활용한다. 그 이유는 31이 소수이면 어떤 수에 31을 곱하는 것을 shift 연산자로 빠르게 계산할 수 있기 때문이다. 예를 들어 31N = 32N - N 에서 2^5은 32니까 31N = (N<<5) - N 으로 표현할 수 있다.
REFERENCES
https://tecoble.techcourse.co.kr/post/2020-07-29-equals-and-hashCode/
반응형