티스토리 뷰
Java Application을 개발하면서 equals()와 hashCode()를 같이 재정의 해야 한다는 말을 들어보신 적 있으신가요?
대부분의 IDE(=Ecilpse, Intellij 등)에서도 equals()와 hashCode()를 같이 재정의 하도록 되어있으며, Lombok에서도 @EqualsAndHashCode을 지원하고 있습니다.
그렇다면, 왜 equals()와 hashCode()는 같이 정의해야 할까요? 천천히 알아보겠습니다.
equals()만 재정의 하는 경우
public class Car {
private final String name;
public Car(String name) {
this.name = name;
}
// intellij Generate 기능 사용
@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);
}
}
Car 클래스에는 equals()만 재정의 해두었습니다. 그리고 객체를 생성하여 equals()를 호출해보도록 하겠습니다.
public static void main(String[] args){
Car car1 = new Car("foo");
Car car2 = new Car("foo");
// true 출력
System.out.println(car1.equals(car2));
}
equals()를 재정의 했기 때문에 name이 같은 car1, car2 객체는 논리적으로 같은 객체로 판단됩니다.
Object 클래스의 equals()
equals()를 재정의 하지 않으면 기본적으로 Object의 equals()를 호출하게 됩니다. 이 Object의 equals()는 동등성이 아닌 동일성을 판별하는 메서드입니다.
즉, 객체의 값을 비교하는 것이 아니라 참조 값(=주소)를 비교하게 됩니다.
그럼, 이제 아래의 main 메서드의 출력 결과를 예측해보겠습니다.
public static void main(String[] args) {
List<Car> cars = new ArrayList<>();
cars.add(new Car("foo"));
cars.add(new Car("foo"));
System.out.println(cars.size());
}
따로 생성한 Car 객체 2개를 List에 추가 하였기 때문에 당연히 Size에 대한 출력 결과는 2입니다.
그럼 이번에 요구사항이 바뀌어 Collection에 중복되지 않는 Car 객체만 넣으라고 추가되었다고 가정해봅시다. 해당 요구사항을 반영하기 위해 중복을 허용하지 않는 Set을 사용하기로 하였습니다.
마찬가지로, 아래 main 메서드의 출력 결과를 예상해봅시다.
public static void main(String[] args) {
Set<Car> cars = new HashSet<>();
cars.add(new Car("foo"));
cars.add(new Car("foo"));
System.out.println(cars.size()); //2
}
추가된 두 Car 객체의 이름이 같기 때문에 논리적으로 같은 객체라 판단하고 HashSet의 size가 1이 나올거라 예상했지만, 예상과 달리 2가 출력되었습니다.
hashCode()를 equals()와 함께 재정의하지 않으면 코드가 예상과 다르게 작동하는 문제를 일으키게 됩니다.
정확히 말하자면, hash 값을 사용하는 Collection(HashSet, HashMap, HashTable)을 사용할 때 문제가 발생합니다.
왜 그럴까요?
앞서 말한 hash 값을 사용하는 Collection(HashSet, HashMap, HashTable)은 객체가 논리적으로 같은지 비교할 때 아래 그림과 같은 과정을 거치게 됩니다.
우선, hashCode()의 리턴 값이 일치하고 equals()의 리턴 값이 true이어야 논리적으로 같은 객체라고 판단합니다.
앞서 본 main 메서드의 HashSet에 Car 객체를 추가할 때도 마찬가지입니다. 다만 Car 클래스에는 hashCode()가 재정의 되어있지 않아 Object 클래스의 hashCode()가 사용되었습니다.
Object 클래스의 hashCode()는 객체의 고유한 주소 값을 int 값으로 변환하기 때문에 객체마다 다른 값을 리턴합니다.
두 Car 객체는 equals()로 비교도 하기 전에 서로 다른 hashCode()의 리턴 값으로 인해 다른 객체로 판단된 것입니다.
hashCode 재정의
그럼 이제, 위에서 살펴본 문제를 해결하기 위해 Car 클래스에 hashCode()를 재정의 해보겠습니다.
public class Car {
private final String name;
public Car(String name) {
this.name = name;
}
// intellij Generate 기능 사용
@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);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
반환 값인 Objects.hash()(주의! Object 클래스가 아닙니다.)는 hashCode()를 재정의 하기 위해 간편히 사용할 수 있는 메서드이지만 속도가 느리다는 단점이 있습니다.
인자를 담기 위한 배열이 만들어지고 인자 중 기본 타입이 있다면 Boxing과 Unboxing의 과정을 거치게 되기 때문입니다.
성능에 민감하지 않다면 Objects.hash()를 사용하여 hashCode()를 재정의해도 문제가 없지만, 민감한 경우라면 직접 hashCode()를 재정의 해야 합니다.
그럼 무조건 같이 재정의 해줘야 하나?
hash 값을 사용하는 Collection을 사용하지 않는 다면, equals()와 hashCode()를 같이 재정의 하지 않아도 되는 건가? 라고 생각할 수 있습니다.
무조건! 재정의 하라고 단정 지을 수는 없습니다.
하지만, 요구사항은 항상 변하기도 하고, 협업 환경이라면 동료는 당연히 equals()와 hashCode()를 같이 재정의 했을 거라 생각하고, hash 값을 사용하는 Collection으로 수정할 가능성도 있습니다.
따라서, equals()와 hashCode()를 같이 재정의 하기를 추천 드립니다.
'Language > JAVA' 카테고리의 다른 글
[JAVA]Thread란? (0) | 2024.09.09 |
---|---|
[JAVA]Object가 아닌 Objects 클래스 (0) | 2024.09.03 |
[JAVA]Optional? Optional의 개념 및 사용법 (0) | 2024.08.28 |
[JAVA]JDK? JRE? JVM? (0) | 2024.08.28 |
[JAVA]JVM이란? (0) | 2024.08.28 |
- Total
- Today
- Yesterday