아이템 10 - equals는 일반 규약을 지켜 재정의하라
다음 상황중 하나에 해당한다면 equals를 굳이 재정의하지 않는 것이 최선
1.각 인스턴스가 본질적으로 고유하다.
- 객체가 고유한 개체라면, 모든 객체는 자신과만 같고, 다른 객체와는 절대 같을 수 없다. 예를 들어, Thread, Socket 같은 시스템 자원 핸들러나 고유한 상태를 가진 객체들은 인스턴스마다 서로 다른 개체로 취급되므로 equals를 재정의할 필요가 없다. 이런 객체는 동등성을 비교하는 게 무의미하기 때문에 equals 메서드를 굳이 재정의하지 않아도 된다.
2. 인스턴스의 논리적 동치성을 검사할 일이 없다.
- 논리적 동등성이란 두 객체가 값으로 비교될 때 같다고 판단되는 경우를 말한다. 하지만 이 규칙에 해당하는 경우는 논리적 동등성을 확인할 일이 없는 클래스들이다. 예를 들어, 주로 상태가 변화하는 엔티티(상태가 계속 변하는 실시간 객체)나 외부 리소스를 제어하는 객체라면, 논리적으로 같다고 판단할 일이 없기 때문에 equals를 재정의할 필요가 없다.
3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
- 상위 클래스에서 이미 equals가 적절히 재정의되어 있고, 하위 클래스에서도 이 구현을 그대로 사용할 수 있는 경우엔, 다시 재정의할 필요가 없다. 즉, 상위 클래스의 동등성 비교 논리가 하위 클래스에도 완벽하게 적용될 때는 상속받은 equals를 그대로 사용하면 된다. 이 경우 상위 클래스의 equals가 하위 클래스에도 충분히 적합하다.
4. 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.
- 클래스가 private이거나 package-private(같은 패키지 내에서만 접근 가능한)이라면, 그 클래스는 외부에서 접근하거나 사용할 일이 거의 없다. 따라서 그 클래스의 인스턴스를 비교할 상황이 아예 발생하지 않으므로 equals 메서드를 재정의할 필요가 없다. 외부 코드에서 호출할 일이 없기 때문에 굳이 재정의하지 않아도 된다.
equals를 재정의할 때 지켜야 할 일반 규약
equals 메서드는 객체의 동등성을 비교할 때 사용된다. 하지만 올바르게 재정의하지 않으면 예기치 못한 버그가 발생할 수 있다. 따라서 equals를 재정의할 때는 다음 다섯 가지 규약을 반드시 지켜야 한다.
- 반사성(reflexivity)
- 동일한 객체는 항상 자신과 같아야 한다.
- a.equals(a)는 항상 true를 반환해야 한다.
- 대칭성(symmetry)
- 두 객체가 서로 같다고 판단되면, 그 순서와 상관없이 결과는 동일해야 한다.
- a.equals(b)가 true면 b.equals(a)도 true여야 한다.
- 추이성(transitivity)
- A와 B가 같고, B와 C가 같다면, A와 C도 같아야 한다.
- a.equals(b)가 true이고, b.equals(c)가 true이면 a.equals(c)도 true여야 한다.
- 일관성(consistency)
- 동일한 객체의 상태에서 equals 결과는 항상 일관되게 반환되어야 한다.
- 두 객체가 같으면 여러 번 호출해도 항상 true, 다르면 항상 false여야 한다.
- null에 대한 비대칭성
- 어떤 객체도 null과 같을 수 없다.
- a.equals(null)은 항상 false여야 한다.
equals 재정의 가이드라인
1.== 연산자를 사용해 입력이 자기 자신인지 확인
- 이 최적화는 성능을 높이고, 동일한 객체는 항상 같다는 반사성을 보장한다.
if (this == obj) {
return true;
}
2.입력 객체가 올바른 타입인지 확인
- 같은 클래스의 인스턴스가 아니라면 false를 반환한다.
if (!(obj instanceof MyClass)) {
return false;
}
3.입력 객체를 적절한 타입으로 형변환
- 캐스팅하여 타입이 맞는지 확인한 후 논리적 동등성 비교를 시작한다.
MyClass other = (MyClass) obj;
4.객체의 핵심 필드들이 모두 일치하는지 확인
- equals 메서드는 해당 클래스에서 중요한 필드들의 동등성을 비교해야 한다.
return this.field1.equals(other.field1) && this.field2 == other.field2;
5.Primitive 타입 비교
Primitive 타입 중에서 float와 double을 제외한 모든 primitive 타입은 == 연산자를 사용해 비교할 수 있다. 하지만 float과 double은 특수한 값들로 인해 == 연산자가 부정확할 수 있다.
public class PrimitiveComparison {
public static void main(String[] args) {
// int 타입은 ==으로 비교 가능
int x = 100;
int y = 100;
System.out.println(x == y); // true
// float 타입의 비교는 특수한 경우를 조심해야 함
float a = 0.0f / 0.0f; // NaN
float b = Float.NaN;
System.out.println(a == b); // false (NaN은 ==으로 비교 불가능)
// Double.compare로 비교하는 방법
double c = 0.0;
double d = -0.0;
System.out.println(c == d); // true (하지만 논리적으로 다름)
System.out.println(Double.compare(c, d)); // 1 (실제로는 다른 값임)
}
}
- float과 double의 특수 값들은 ==으로 비교할 수 없기 때문에 compare 메서드를 사용한다.
equals를 다 구현했다면 우리는 세 가지를 자문해볼 수 있다.
1.대칭적인가?
2.추이성이 있는가?
3.일관적인가?
핵심 정리
꼭 필요한 경우가 아니면 equals를 재정의하지 말자.많은 경우에 Object의 equals가 여러분이 원하는 비교를 정확히 수행해준다. 재정의해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약을 확실히 지켜가며 비교해야 한다.
'Java' 카테고리의 다른 글
이펙티브 자바(Effective Java) 2장 - 아이템 12 toString을 항상 재정의하라 (0) | 2024.12.03 |
---|---|
이펙티브 자바(Effective Java) 2장 - 아이템 11 equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2024.12.03 |
이펙티브 자바(Effective Java) 2장 - 아이템 9 try-finally보다는 try-with-resources를 사용하라 (0) | 2024.12.03 |
이펙티브 자바(Effective Java) 2장 - 아이템 8 finalizer와 cleaner 사용을 피하라 (0) | 2024.12.03 |
이펙티브 자바(Effective Java) 2장 - 아이템 7 다 쓴 객체 참조를 해제하라 (0) | 2024.12.03 |