1. 반사성 : null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.
2. 대칭성 : null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
3. 추이성 : null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y)가 true이고 y.equals(z)도 true면 x.equals(z)도 true다.
4. 일관성 : null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환함
5. null-아님 : null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.
1. 반사성
- 객체는 자기 자신과 같아야 한다는 뜻
2. 대칭성
- 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다
코드 10-1 잘못된 코드 - 대칭성 위배!
// 코드 10-1 잘못된 코드 - 대칭성 위배! (54-55쪽)
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s) {
this.s = Objects.requireNonNull(s);
}
// 대칭성 위배!
@Override public boolean equals(Object o) {
if (o instanceof CaseInsensitiveString)
return s.equalsIgnoreCase(
((CaseInsensitiveString) o).s);
if (o instanceof String) // 한 방향으로만 작동한다!
return s.equalsIgnoreCase((String) o);
return false;
}
... //코드 생략
}
- CaseInsensitiveString의 equals는 일반 문자열과도 비교를 시도한다
CaseInsensitiveString과 일반 String 객체가 하나씩 있다고 해보자!
CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
String s = "polish";
- cis.equals(s)는 true를 반환한다
- 문제는 CaseInsensitiveString의 equals는 일반 String을 알고 있지만 String의 equals는 CaseInsensitiveString 모른다
- s.equals(cis)는 false를 반환한다
CaseInsensitiveString을 컬렉션에 넣어보자!
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
- OpenJDK 버전이 바뀌거나 다른 JDK에서는 true를 반환하거나 런타임 예외를 던질 수도 있다
- equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.
이런 문제를 해결하려면?
// 수정한 equals 메서드 (56쪽)
@Override public boolean equals(Object o) {
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
3. 추이성
- 첫 번째 객체와 두 번째 객체가 같고, 두번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체도 같아야 한다
ex) a=b, b=c -> a=c
2차원에서의 점을 표현하는 클래스
// 단순한 불변 2차원 정수 점(point) 클래스 (56쪽)
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;
...// 코드 생략
}
- Point의 구현이 상속되어 색상 정보는 무시한 채 비교를 수행한다
- equals 규약을 어긴 것은 아니지만, 중요한 정보를 놓치게 되어 받아들일 수 없는 상황이다
비교대상이 또 다른 ColorPoint이고 위치와 색상이 같은 때만 true를 반환하는 equals를 생각해보자!
코드 10-2 잘못된 코드 - 대칭성 위배!
// 코드 10-2 잘못된 코드 - 대칭성 위배! (57쪽)
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
return super.equals(o) && ((ColorPoint) o).color == color;
}
- Point를 ColorPoint에 비교한 결과와 그 둘을 바꿔 비교한 결과가 다를 수 있다
- Point의 equals는 색상을 무시하고, ColorPoint의 equals는 입력 매개변수의 클래스 종류가 다르다며 false를 반환할 것이다
각각의 인스턴스를 하나씩 만들어 실제로 동작하는 모습을 확인해보자.
// 첫 번째 equals 메서드(코드 10-2)는 대칭성을 위배한다. (57쪽)
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
- p.equals(cp)는 true를, cp.equals(p)는 false를 반환한다
코드 10-3 잘못된 코드 - 추이성 위배!
//코드 10-3 잘못된 코드 - 추이성 위배! (57쪽)
@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;
}
- 대칭성을 지켜주지만, 추이 성을 깨버린다
// 두 번째 equals 메서드(코드 10-3)는 추이성을 위배한다. (57쪽)
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
- p1.equals(p2)와 p2.equals(p3)는 true를 반환하는데, p1.equals(p3)가 false를 반환한다
- p1과 p2와, p2와 p3 비교에서는 색상을 무시했지만 p1과 p3 비교에서는 색상을 고려하기 때문이다
- 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다
코드 10-4 잘못된 코드 - 리스 코프 치환 원칙(59쪽) 위배
// 잘못된 코드 - 리스코프 치환 원칙 위배! (59쪽)
@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;
}
- 이번 equals는 같은 구현 클래스의 객체와 비교할 때만 true를 반환한다
- 실제로는 사용할 수 없다
예를 들어 주어진 점이 (반지름 1인) 단위 원 안에 있는지 를 판별하는 메서드가 필요하다
// 단위 원 안의 모든 점을 포함하도록 unitCircle을 초기화한다. (58쪽)
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);
}
- 이제 값을 추가하지 않는 방식으로 Point를 확장하겠다
// Point의 평범한 하위 클래스 - 값 컴포넌트를 추가하지 않았다. (59쪽)
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(); }
}
- 리스 코프 치환 원칙에 따르면, 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요
- 따라서 그 타입의 모든 메서드가 하위 타입에서도 똑같이 잘 작동해야 한다
- "Point의 하위 클래스는 정의상 여전히 Point이므로 어디서든 Point로 써 활용될 수 있어야 한다"
- 그런데 CounterPoint의 인스턴스를 onUnitCircle 메서드에 넘기면 어떻게 될까?
- Point 클래스의 equals를 getclass를 사용해 작성했다면 onUnitCircle은 false를 반환할 것이다
- CounterPoint 인스턴스 x, y값과는 무관하게 말이다
Why?
- 원인은 컬렉션 구현체에서 주어진 원소를 담고 있는지를 확인하는 방법에 있다
- onUnitCircle에서 사용한 Set을 포함하여 대부분의 컬렉션은 이 작업에 equals 메서드를 이용하는데,
- CounterPoint의 인스턴스는 어떤 Point와도 같을 수 없기 때문이다
- 반면 Point의 equals를 instanceof 기반으로 올바르게 구현했다면 CounterPoint 인스턴스를 건네줘도 onUnitCircle 메서드가 작동
구체 클래스의 하위 클래스에서 값을 추가할 방법은 없지만 괜찮은 우회 방법이 하나 있다
코드 10-5 equals 규약을 지키면서 값 추가하기
// 코드 10-5 equals 규약을 지키면서 값 추가하기 (60쪽)
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
/**
* 이 ColorPoint의 Point 뷰를 반환한다.
*/
public Point asPoint() {
return point;
}
@Override public boolean equals(Object o) {
if (!(o instanceof ColorPoint))
return false;
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
@Override public int hashCode() {
return 31 * point.hashCode() + color.hashCode();
}
}
- "상속 대신 컴포지션을 사용하다"
- Point를 상속하는 대신 Point를 ColorPoint의 private 필드로 두고,
- ColorPoint와 같은 위치의 일반 Point를 반환하는 뷰 메서드를 public 추가하는 식
자바 라이브러리에도 구체 클래스를 확장해 값을 추가한 클래스가 종종 있다.
- java.sql.Timestamp의 equals는 대칭성을 위배하며, Date 객체와 한 컬렉션에 넣거나 서로 섞어 사용하면 엉뚱하게 동작한다
'Books > Effective-Java 3판' 카테고리의 다른 글
11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2021.10.02 |
---|---|
10.equals는 일반 규약을 지켜 재정의하라(3) (0) | 2021.10.01 |
10.equals는 일반 규약을 지켜 재정의하라(1) (0) | 2021.09.28 |
9.try-finally보다는 try-with-resources를 사용하라 (0) | 2021.09.27 |
8.finalizer와 cleaner 사용을 피하라 (0) | 2021.09.25 |