[Effective Java] 아이템10 - equals는 일반 규약을 지켜 재정의하라
3장은 Object 클래스에서 final이 아닌 메서드 (equals, hashCode, toString, clone, finalize) 의 재정의에 대해서 설명한다. 모든 클래스는 이 메서드들을 일반 규약에 맞게 재정의해야 한다.
equals 메서드는 되도록이면 재정의하지 말자
다음과 같은 상황에서는 equals 메서드를 재정의하지 않는 것이 최선이다. (굳이 그럴 필요가 없다는 말이다.)
Thread와 같이 값이 아니라 동작하는 개체- 논리적 동치성을 검사할 일이 없으면 굳이 equals 메서드도 필요 없다
- 상위 클래스의 equals가 하위 클래스에도 적용이 가능하다면 굳이 재정의할 필요 없다.
- Set 구현체는 AbstractSet의 equals를 상속받아 쓴다.
아래에서 설명한 equals 메서드 작성 시 규칙은 복잡합니다.
AbstractSet의 equals 메소드를 살펴보니 책에서 말하는 규칙을 모두 지킨 것 같아 먼저 살펴봅니다.
public boolean equals(Object o) {
if (o == this) // (1) First check if the specified object is this set
return true;
if (!(o instanceof Set)) // (2) instanceof
return false;
Collection<?> c = (Collection<?>) o; // (3) 형변환
if (c.size() != size()) // (4) Then check if size is identical
return false;
try {
return containsAll(c); // (5) iterates over all
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
그러면 equals 메서드는 언제 재정의 하는게 좋을까요?
간단합니다! 논리적 동치성을 확인해야 하는데, 상위클래스가 equals를 재정의하지 않았을 경우에 재정의해서 쓰면 됩니다.
equals 메서드 재정의 규칙
객체 A와 객체 B가 동일한지 체크하고자 할 때 equals 메소드를 재정의해서 사용한다고 했습니다.
하지만 이 equals 메서드를 재정의할 때, 아래와 같은 규칙을 따라야 끔찍한 문제가 생기지 않습니다.
다소 수학적인 논리개념이 들어가는데, 참고만 하면 될 것 같습니다. (구현한 내용을 보는게 더 이해가 빠를 것 같아서..)
equals 메서드 정의 규칙
equals 메서드는 동치관계(equivalence relation)를 구현하며,다음을 만족한다
- 반사성 (reflexivity) : x.equals(x)가 true이다.
- 객체는 자기 자신과 같아야 한다.
- 대칭성 (symmetry) : x.equals(y)가 true이면 y.equals(x)도 true이다.
- 대소문자를 무시하는
CaseInsensitiveString클래스를 만들고 equals 메소드도 대소문자를 무시하고 비교하는 경우 - <대소문자 무시="" 클래스="">.equals(String str) = true 이더라도 String str.equals(<대소문자 무시클래스="">)가 false이므로 대소문자>대소문자>
- String과 연동할 수 없기 때문에,
CaseInsensitiveString끼리 비교했을 때 동일했을때만 true를 반환하도록 해야한다.
- 대소문자를 무시하는
- 추이성 (transitivity) : x.equals(y) = true , y.equals(z) = true 이면 x.equals(z)도 true이다.
- 상위 클래스에는 없는 새로운 필드가 하위 클래스에 있을 경우 equals가 다른 결과를 반환할 수 있다.
- p.equals(cp) = true, cp.equals(p) = false를 반환한다.
Point p = new Point(1, 2); ColorPoint cp = new ColorPoint(l, 2, Color.RED); // Point 클래스 상속한 ColorPoint 클래스 - 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
java.sql.Timestamp는java.util.Date를 확장했는데 그래서 Timestamp의 equals는 대칭성을 위배하여 사용에 주의해야 한다.
- 상속 대신 컴포지션을 사용하면 된다. [아이템18]
public class ColorPoint { private final Point point; private final Color color; }
- 일관성 (consistency) : x.equals(y)를 반복해서 호출해도 항상 true를 반환하거나 false를 반환한다.
java.net.URL의 equals는 URL과 매핑된 호스트의 IP주소를 이용해 비교한다. –> 일관성 위반Note: The defined behavior for {@code equals} is known to be inconsistent with virtual hosting in HTTP.
- null-아님 : null이 아닌 모든 참조 값 x에 대해, x.equals(null) 은 false이다.
- 모든 객체가 null과 같지 않아야 한다.
equals 메서드 구현 방법
- == 연산자를 사용해 입력이 자기 자신의 참조인지 확인 (reflexivity)
instanceof연산자로 입력이 올바른 타입인지 확인- 입력을 올바른 타입으로 형변환 (2번과 같은 맥락, 2번에서 통과했으면 이 단계는 100% 성공)
- 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 확인
위에 언급했던 AbstractSet 도 위와 같은 패턴, 규칙으로 equals 메서드를 재정의한 것이 이제 보이죠?
자바에서 equals 메서드를 구현한 것은 모두 이 패턴으로 구현된 것 같습니다.
Note: float 과 double은 참조타입으로 비교하자 Float.compare(float, float), Double,compare(double, double)
배열은 Arrays.equals 메서드를 사용하자.
Tip
최상의 성능을 바란다면 다를 가능성이 더 크거나 비교하는 비용이 싼 필드를 먼저 비교하자. 그 내용을 캐쉬로 저장해두는 것도 방법입니다.
AbstractSet이 모든 원소를 iterate 하기 전에 (4) 에서 size를 먼저 비교한것처럼 말이죠!
다음은 위 규약을 모두 지켜서 만든 equals 재정의 메소드이다.
@Override public boolean equals(Object o) {
if (o = this) return true;
if (!(o instanceof PhoneNumber)) return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix = prefix && pn.areaCode = areaCode;
}
equals 메소드를 재정의 했다면, 테스트를 해봐야 한다. 구글의 AutoValue 프레임워크를 사용하면 보다 쉽게 테스트할 수 있다.
Lombok 라이브러리의 @EqualsAndHashCode
Lombok에서 제공하는 @EqualsAndHashCode 어노테이션은 hashCode 와 equals 메소드를 자동으로 생성해줍니다.
클래스의 모든 non-static, non-transient 필드가 대상이며, EqualsAndHashCode.Include 또는 EqualsAndHashCode.Exclude로 특정 필드를 포함시키거나 제외시킬 수 있습니다.
callSuper=true 옵션을 줘서 상위클래스의 equals 와 hashCode 도 포함시킬 수 있습니다.
cacheStrategy를 써서 hashCode의 결과값을 캐싱해서 비교할 때 활용할 수 있습니다.
@Override public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof EqualsAndHashCodeExample)) return false;
EqualsAndHashCodeExample other = (EqualsAndHashCodeExample) o;
if (!other.canEqual((Object)this)) return false;
if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;
if (Double.compare(this.score, other.score) != 0) return false;
if (!Arrays.deepEquals(this.tags, other.tags)) return false;
return true;
}
코드를 보면, 롬복도 책에서 말하고 있는 패턴과 유사하게 equals 메서드를 재정의한 것을 확인할 수 있습니다.
TODO
여러 필드를 담고있는 클래스의 equals 메소드를 재정의 해보자
댓글남기기