2023. 11. 21. 02:44ㆍBook/이펙티브 자바
3. 추이성 : 첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면 첫 번재 객체와 세 번째 객체도 같아야 한다는 뜻이다. 이 요건도 간단하지만 자칫하면 어기기 쉽다. 상위 클래스에는 없는 새로운 필드를 하위 클래스에 추가하는 상황을 생각해보자.
public 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 p.x == x && p.y == y;
}
}
이제 이 클래스를 확장해서 점에 색상을 더해보자.
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
...// 나머지 코드는 생략
}
상속받은 클래스에서 equals 메서드를 재정의하지 않는다면 부모 클래스의 equals 메서드를 사용하여 x, y 필드만 비교할 것이다.
다음 코드처럼 비교 대상이 또 다른 ColorPoint이고 위치와 색상이 같을 때만 true를 반환하는 eqauls를 재정의 해 보자
//잘못된 코드 대칭성 위배(10-2)
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
public static void main(String[] args) {
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
System.out.println(p.equals(cp) + " " + cp.equals(p));
}
==>
true false
위 코드는 ColorPoint 클래스 타입이 아니면 false를 리턴하고 맞다면 부모 equals 메서드를 이용해 x, y를 비교하고 색상도 비교한다.
p.equals(cp)는 상속 관계이므로 x, y를 비교 후 true를 리턴한다. 하지만
cp.equals(p)는 상속 관계이지만 ColorPoint클래스가 구체 클래스이므로 첫 번째 조건에서 false를 리턴하고 Point의 equals는 색상을 무시해 대칭성을 위배한다.
그렇다면 ColorPoint.equals에서 Point와 비교할 때는 색상을 무시하도록 해보자
//추이성 위배(10-3)
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
//o가 일반 Point면 색상을 무시하고 비교한다. (매우매우매우 위험! 무한재귀 위험있음)
if (!(o instanceof ColorPoint)) return o.equals(this);
//o가 ColorPoint면 색상까지 비교한다.
return super.equals(o) && ((ColorPoint) o).color == color;
}
public static void main(String[] args) {
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
System.out.println(p.equals(cp) + " " + cp.equals(p));
}
==> true true
public static void main(String[] args) {
//두 번재 equals 메서드는 추이성을 위배한다.(10-3)
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
System.out.printf("%s %s %s%n", p1.equals(p2), p2.equals(p3), p1.equals(p3));
}
==> true true false
이 방식은 대칭성은 지켜주지만 추이성을 깨버린다.
p1과 p2, p2와 p3는 색상을 무시해서 true를 반환하지만 p1과 p3는 색상을 비교해서 false를 반환한다. 또한 PointColor.equals 메서드의 두 번째 조건에서 Point 클래스의 서브 클래스가 o 라면 무한 재귀에 빠진다.
그럼 해법은 무엇일까? => 없다..
구체 클래스를 확장하고 거기에 새로운 필드를 추가하면서 equals 규약을 만족시킬 방법은 없다. 객체 지향적 추상화의 이점을 포기하면 가능하다.
public 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 == null || o.getClass() != getClass()) return false;
Point p = (Point) o;
return p.x == x && p.y == y;
}
}
==========================================================================================
public class CounterPoint extends Point {
private static final AtomicInteger counter = new AtomicInteger();
public CounterPoint(int x, int y) {
super(x, y);
counter.incrementAndGet();
}
public static int numberCreated() { return counter.get(); }
}
이번 equals는 같은 구현 클래스의 객체와 비교할 때만 true를 반환한다. 괜찮아 보이지만 그렇지 않다.
단순히 unitCircle의 필드의 Set 자료 구조에 해당 객체가 있는지 없는지 확인하는 메서드를 테스트 해보자
public class CounterPointerTest {
private static final Set<Point> unitCircle = Set.of(
new Point(1, 0), new Point(0, 1),
new Point(-1, 0), new Point(0, -1)
);
public static boolean onUnitCircle(Point p) {
return unitCircle.contains(p);
}
public static void main(String[] args) {
Point p1 = new Point(1, 0);
Point p2 = new CounterPoint(1, 0);
// true를 출력한다.
System.out.println(onUnitCircle(p1));
// true를 출력해야 하지만, Point의 equals가 getClass를 사용해 작성되었다면 그렇지 않다.
System.out.println(onUnitCircle(p2));
}
}
리스코프 치환 원칙에 따르면, 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하다. 따라서 그 타입의 모든 메서드가 하위 타입에서도 똑같이 잘 작동해야 한다.
그런데 CounterPoint의 인스턴스를 onUnitCircle 메서드에 넘기면 어떻게 될까? Point 클래스의 equals는 getClass를 사용해 작성했다면 false를 리턴할 것이다.
이유는 대부분의 컬렉션은 이 작업에 equals 메서드를 이용하는데, CounterPoint의 인스턴스는 어떤 Point와도 같을 수 없기 때문이다. (instanceof 가 아닌 getClass 비교에서)
구체 클래스의 하위 클래스에서 값을 추가할 방법은 없지만 괜찮은 우회 방법이 하나 있다.
"상속 대신 컴포지션을 사용하라"는 아이템 18의 조언을 따르면 된다....
'Book > 이펙티브 자바' 카테고리의 다른 글
Item10 - equals는 일반 규약을 지켜 재정의하라(4) (1) | 2023.11.22 |
---|---|
Item10 - equals는 일반 규약을 지켜 재정의하라(3) (1) | 2023.11.21 |
Item10 - equals는 일반 규약을 지켜 재정의하라(1) (1) | 2023.11.20 |
Item9 - try ~ finally 보다 try-with-resources를 사용하라 (1) | 2023.11.19 |
Item8 - finalizer와 cleaner 사용을 피하라 (1) | 2023.11.19 |