[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 메소드를 재정의 해보자
댓글남기기