equals를 재정의할 때 곳곳에 함정이 존재하므로 주의해야 한다.
다음과 같은 상황들에서는 equals를 재정의하지 않아야 한다.
equals를 재정의하지 않는 것은 인스턴스는 오직 자기 자신과만 같다는 것이다.
재정의 하지 말아야 할 때
1. 각 인스턴스가 본질적으로 고유할 때
값을 표현하는 게 아니라 동작하는 개체를 표현하는 클래스에서는 재정의를 하지 않는다.
예를 들어 controller, view 등등
이미 Object에 equals 메서드가 잘 구현되어있다.
// Object의 equals 메소드
public boolean equals(Object obj) {
return (this == obj);
}
2. 인스턴스의 '논리적 동치성'을 검사할 일이 없다.
3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
예 :
AbstractSet이 구현한 equals를 Set 구현체에서 상속받아서 사용
// 추상 클래스 AbstractSet의 equals 메서드
// 이 메서드를 Set 구현체에서도 사용한다.
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection<?> c = (Collection<?>) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException | NullPointerException unused) {
return false;
}
}
4. 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.
5. 인스턴스 통제 클래스 ( Enum )
인스턴스 통제 클래스라면 어차피 논리적으로 같은 인스턴스가 2개 이상 만들어지지 않으므로 논리적 동치성을 확인할 필요가 없다.
재정의 해야할 때
1. 값이 같은지 알고싶을 때
객체가 같은지가 아니라 값이 같은지 알고 싶을 때 equals를 재정의해야 한다.
equals가 논리적 동치성을 확인하도록 재정의해두면, Map의 키와 Set의 원소로 사용할 수 있게 된다.
equals 메서드 재정의 시 일반 규약
- 반사성 : x.equals(x) = true ( x != null )
- 대칭성 : x.equals(y) = y.equals(x) ( x != null, y != null )
- 추이성 : x.equals(y) = true, y.equals(z) = true -> x.equals(z) = true
- 일관성 : x.equals(y) 는 항상 일정
- null-아님 : x.equals(null) = false
null을 검사하기 위해서 instanceof를 사용하면 된다!
ex)
public boolean equals(Object o) {
if (!(o instanceof MyType)) {
return false;
}
...
}
위의 equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할 지 알 수 없다.
예를 들어
x.equals(y) = true
y.equals(x) = false
인 x, y가 존재한다고 할 때
List list = new ArrayList();
list.add(x);
// 아래의 결과는 true 일까? false일까?
list.contains(y)
equals 메서드 일반 규약을 어긴 예
class Point2D {
private final int x;
private final int y;
public Point2D(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point2D)) {
return false;
}
Point2D p = (Point2D) obj;
return p.x == x && p.y == y;
}
}
// Point2D를 상속받은 Point3D
class Point3D extends Point2D {
private final int z;
public Point3D(int x, int y, int z) {
super(x, y);
this.z = z;
}
// Point3D는 필드변수 z까지 같아야함!
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Point3D)) {
return false;
}
return super.equals(obj) && ((Point3D) obj).z == z;
}
}
이 두 클래스에 대해서 equals를 실행하면 다음과 같다.
public class main {
public static void main(String[] args) {
Point2D p2 = new Point2D(1, 2);
Point3D p3 = new Point3D(1, 2, 3);
System.out.println(p2.equals(p3)); //true
System.out.println(p3.equals(p2)); //false
}
}
위를 통해 대칭성을 위배한 경우를 확인할 수 있다.
객체 지향적 추상화의 이점 (추상 클래스 상속)을 포기하지 않는 한
이런식으로 구체 클래스를 확장해 새로운 값을 추가하면서 eqauls 규약을 만족시킬 방법은 존재하지 않는다.
추상 클래스의 하위 클래스에서 새롭게 필드 변수가 추가되므로 새롭게 생긴 필드 변수에 대해서는 equals를 적용할 수 없게 되므로..
이 문제를 우회적으로 해결하기 위해서는 상속 대신 컴포지션을 활용하면 된다. (아이템 18)
equals 메서드 구현 방법
이상적인 equals 메서드의 예
class Point2D {
private final int x;
private final int y;
public Point2D(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj) {
// 1
if (obj == this) {
return true;
}
// 2
if (!(obj instanceof Point2D)) {
return false;
}
//3
Point2D p = (Point2D) obj;
//4
return p.x == x && p.y == y;
}
}
1. == 연산자를 통해 자기 자신의 참조인지 확인한다.
단순 성능 최적화용
2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
입력이 올바르지 않다면 false를 반환한다.
3. 입력을 올바른 타입으로 형변환한다.
2번에서 instanceof 검사를 했으므로 이 변환은 100% 성공
4. 핵심 필드들이 모두 일치하는지 검사한다.
이를 통해 최종적으로 true인지 false인지 확인한다.
필드 들을 비교할 때는 다를 가능성이 크거나 비교하는 비용이 싼 순서대로 비교한다.
5. equals를 다 구현하고, 대칭적인가? 추이성이 있는가? 일관적인가? 확인한다.
6. equals를 재정의할 땐 hashCode도 반드시 재정의하자
추가 사항
1. float와 double은 특수한 부동소수 값을 다뤄야 하므로
Float.compare(float, float)
Double.compare(double, double)
로 비교한다.
2. 입력 타입은 반드시 Object여야 한다.
잘못된 예 :
@Override
public boolean equals(Point2D p2) {
위의 메서드는 Object.equals를 재정의한 것이 아니라 컴파일 에러가 발생한다.
이 메서드는 eqauls를 다중 정의한 것이다. 타입을 구체적으로 명시하지말자.
핵심
꼭 필요한 경우가 아니면 equals를 재정의하지말자.
재정의할거면 핵심 필드를 모두 빠짐없이 규약을 지켜가며 비교하자.
'우아한테크코스 > 공부' 카테고리의 다른 글
스트림 병렬화는 주의해서 적용하라 (0) | 2022.03.12 |
---|---|
람다보다는 메서드 참조를 사용하라 (0) | 2022.03.07 |
값 객체(VO) (0) | 2022.02.20 |
[자동차 경주 미션] 전략 패턴 사용 후기 (0) | 2022.02.18 |
[자동차 경주 미션] 피드백 반영 (0) | 2022.02.13 |
댓글