[Effective Java] 아이템10 - equals는 일반 규약을 지켜 재정의하라

3장은 Object 클래스에서 final이 아닌 메서드 (equals, hashCode, toString, clone, finalize) 의 재정의에 대해서 설명한다. 모든 클래스는 이 메서드들을 일반 규약에 맞게 재정의해야 한다.

equals 메서드는 되도록이면 재정의하지 말자

다음과 같은 상황에서는 equals 메서드를 재정의하지 않는 것이 최선이다. (굳이 그럴 필요가 없다는 말이다.)

  • Thread 와 같이 값이 아니라 동작하는 개체
  • 논리적 동치성을 검사할 일이 없으면 굳이 equals 메서드도 필요 없다
  • 상위 클래스의 equals가 하위 클래스에도 적용이 가능하다면 굳이 재정의할 필요 없다.
    • Set 구현체는 AbstractSet의 equals를 상속받아 쓴다.

아래에서 설명한 equals 메서드 작성 시 규칙은 복잡합니다.
AbstractSetequals 메소드를 살펴보니 책에서 말하는 규칙을 모두 지킨 것 같아 먼저 살펴봅니다.

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 메서드 정의 규칙
  • 반사성 (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.Timestampjava.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 메서드 구현 방법

  1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인 (reflexivity)
  2. instanceof 연산자로 입력이 올바른 타입인지 확인
  3. 입력을 올바른 타입으로 형변환 (2번과 같은 맥락, 2번에서 통과했으면 이 단계는 100% 성공)
  4. 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 확인

위에 언급했던 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 어노테이션은 hashCodeequals 메소드를 자동으로 생성해줍니다.

클래스의 모든 non-static, non-transient 필드가 대상이며, EqualsAndHashCode.Include 또는 EqualsAndHashCode.Exclude로 특정 필드를 포함시키거나 제외시킬 수 있습니다.

callSuper=true 옵션을 줘서 상위클래스의 equalshashCode 도 포함시킬 수 있습니다.

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 메소드를 재정의 해보자

댓글남기기