[Effective Java] 3 - item 10 equals는 일반 규약을 지켜 재정의하라.
item10 equals는 일반 규약을 지켜 재정의하라.
equals()메서드는 재정의가 쉬우나, 잘못 작성하게 되면 의도하지 않는 결과들이 초래되기 쉬우므로 Effective Java에서는 변경하지 않는 것을 권장한다.- 많은 경우에
Object.equals가 원하는 비교를 정확히 수행해 줄 것이다.
- 많은 경우에
재정의하지 않아도 되는 경우
- 각 인스턴스가 본질적으로 고유한 경우
- 값이 아닌 동작을 표현하는 클래스이다. ->
Thread
- 값이 아닌 동작을 표현하는 클래스이다. ->
- 인스턴스의 논리적인 동치성을 검사할 일이 없는 경우
- 상위 클래스에서 재정의한 equals가 하위 클래스에서도 동일하게 적용되는 경우
- 클래스가
private이거나,package-private여서 클라이언트 코드에서equals를 호출할 일이 없다. - 싱글턴임을 보장하는 클래스일때 : 객체간 동등성 및 동일성이 보장된다.
재정의시 규약
반사성 (reflexivity)
- null이 아닌 모든 참조 값 x에 대해
x.equals(x)를 만족한다.
대칭성 (symmetry)
- null이 아닌 모든 참조 값 x,y에 대해,
x.equals(y) == true이면y.equals(x) == true이다.
추이성
- null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y)가 true이고, y.equals(z)가 true이면 x.equals(z)도 true가 되야 한다.
class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof Point)) return false;
Point p = (Point) o;
return this.x == p.x && this.y == p.y;
}
...
}
class ColorPoint extends Point {
private final Color color;
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint)) return false;
return super.equals(o) && this.color == ((ColorPoint) o).color;
}
}
void test() {
ColorPoint a = new ColorPoint(2, 3, Color.RED);
Point b = new Point(2, 3);
ColorPoint c = new ColorPoint(2, 3, Color.BLUE);
System.out.println(a.equals(b)); // true
System.out.println(b.equals(c)); // true
System.out.println(a.equals(c)); // false
}
- a.equals(b)는
true를 만족하고 b.equals(c)는true를 만족하지만 a.equals(c)는false가 된다. - 위의 코드는 equals 정의 규약 중 추이성을 위반하는 코드가 된다.
무한 재귀호출
public class SmellPoint extends Point {
private final Smell smell;
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
// o가 일반 Point이면 색상을 무시햐고 x,y 정보만 비교한다.
if (!(o instanceof SmellPoint)) { // StackOverflowError
return o.equals(this);
}
// o가 ColorPoint이면 색상까지 비교한다.
return super.equals(o) && this.smell == ((SmellPoint) o).smell;
}
}
void test() {
Point cp = new ColorPoint(2, 3, Color.RED);
Point sp = new SmellPoint(2, 3, Smell.SWEET);
System.out.println(cp.equals(sp));
}
- 위와 같이 ColorPoint와 SmellPoint에 대해 equals비교를 한다면
!(o instanceof ColorPoint)에서 무한 재귀 호출이 일어날 것이다.
리스코프 치환 원칙 위반
- 리스코프 원칙 : 부모 타입의 모든 메서드는 하위 타입에서도 잘 작동해야 한다.
public class Point{
@Override public boolean equals(Object o) {
if (o == null || o.getClass() != getClass())
return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
}
-
위의 코드는 같은 구현 클래스 간 비교에서만 참을 반환하게 되고, 상속받는 하위 클래스에서는 거짓을 반환할 것이다.
-
이와 같은 경우, 상속을 사용하지 않고 컴포지션을 활용해 해결할 수 있다.
- 상속에 의해 발생하는 대칭성 위배, 추이성 위배, 리스코프 치환 원칙이 일어나지 않을 것이다.
public ColorPoint {
private Point point;
private Color color;
public ColorPoint(int x, int y, Color color) {
this.point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
public Point asPoint() {
return this.point;
}
@Override
public boolean equals(Object o) {
if(!(o instanceof ColorPoint)) {
return false;
}
ColorPoint cp = (ColorPoint) o;
return this.point.equals(cp) && this.color.equals(cp.color);
}
}
일관성
- null이 아닌 모든 참조 값 x,y에 대해,
x.equals(y)를 반복해도 값이 변하지 않는다.
not null
- null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false이다.
instanceof를 통해 묵시적 null 검사를 해 주는 것이 좋다.
권장 구현 방법
- 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
instanceof연산자로 올바른 타입인지 확인한다.- 올바른 타입으로 형변환한다.
- 객체와 자기 자신의 대응되는 ‘핵심’ 필드들이 모두 일치하는지 하나씩 검사한다.
Leave a comment