[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