[Effective Java] Item10. equals는 일반 규약을 지켜 재정의하라

2022년 03월 14일

TOC

이 글은 Effective Java 3/E의 내용을 요약한 글입니다. 자세한 내용은 책을 참고하시기 바랍니다.

java의 Object들에 재정의를 하는 equals의 경우 잘못 사용하면 끔찍한 결과를 초래할 수 있습니다. 그래서 equals는 객체의 논리적 동치성을 확인해야할 때 빼고 불필요한 경우에는 재정의를 하지 않는 것이 더 좋습니다.

equals를 재정의하지 않아도 되는 상황

  • 각 인스턴스가 본질적으로 고유한 경우
  • 인스턴스의 논리적 동치성(logical equality)를 검사할 일이 없는 경우
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 맞는 경우
  • 클래스가 private, package-private이고 equals메서드를 호출할 일이 없는 경우

    • equals를 호출될 일이 전혀 없는 경우 다음과 같이 막을 수도 있다.

      @Override
      public boolean equals(Object o) {
      throw new AssertionError();
      }

동치관계를 만족시키기 위한 다섯가지 요건

반사성(reflexivity)

  • 객체는 자기 자신과 같아야 한다는 뜻
  • null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true이다.

대칭성(symmetry)

  • 객체는 서로의 동치 여부에 대해 똑같이 답해야 한다는 뜻
  • null이 아닌 모든 참 조 값 x, y에 대해, x.equals(y)가 true이면 y.equals(x)도 true이다.

추이성(transitivity)

  • A==B, B==C이면, A==C이다.
  • null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true이고 y.equals(z)도 true이면 x.equals(z)도 true이다.
  • 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.

    • 단, 추상 클래스의 하위 클래스에서는 equals규약을 지키면서도 값을 추가할 수 있다.
    • 그 외에는 하위 클래스에서 값을 추가할 방법은 없지만, 상속대신 컴포지션을 사용하는 방법을 사용할 수 있다.

일관성(consistency)

  • 두 객체가 같다면 수정되지 않는 한 영원히 같아야 한다.
  • null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
  • 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안 된다.

    • equals는 항시 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야한다.

null아님

  • 모든 객체가 null과 같지 않아야 한다.
  • null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false이다.
  • 해당 검사를 하기 위해 if(object == null) retuen false;와 같은 코드는 사용하지 말고 instanceof를 사용해 검사하자.

    @Override
    public boolean equals(Object o) {
    if (o instanceof MyType) return false;
    MyType mt = (MyType) o;
    ...
    }

좋은 equals메서드를 구현하는 순서

  1. ==연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
  2. instanceof로 입력이 올바른 타입인지 확인한다.
  3. 입력을 올바른 타입으로 형변환 한다.
  4. 입력 객체와 자기 자신의 대응되는 "핵심"필드들이 모두 일치하는지 하나씩 검사한다.
  5. 모든 필드가 일치하면 true, 하나라도 다르면 false반환

equals를 재정의 할 때 주의사항

  • float, double을 제외한 기본 타입 필드는 ==를 통해 비교하고, 참조 타입은 equals메서드로 비교한다. float, doubleFloat.compare(float, float);Double.compare(double, double);로 비교한다. (부동 소수값을 다뤄야하기 때문)
  • 때로는 null도 정상 값으로 취급하는 참조 타입 필드가 존재하여, 이러한 필드는 정적 메서드인 Object.equals(Object, Object);로 비교해 NullPointException을 발생시켜 예방방하는 것이 좋다.
  • 비교하기 아주 복잡한 필드를 가진 클래스들으니 필드의 표준형을 저장해둔 후 표준형끼리 비교하면 경제적이다.
  • 최상의 성능을 바란다면 다를 가능성이 크거나 비교하는 비용이 싼 필드를 먼저 비교하는 것이 좋다. (동기화용 lock 필드같이 객체의 논리적 상태와 관련 없는 필드는 비교하면 안된다.)
  • equals를 다 구현하였으면 대칭적인지, 추이성은 있는지, 일관적인지 체크를 해봐야 한다.
  • equals를 재정의 할 때 hashCode도 반드시 재정의해야 한다.
  • 필드들의 동치성만 확인해도 equals 규약을 어렵지 않게 지킬 수 있음으로 너무 복잡하게 해결하려 들지 않는 것이 좋다.
  • Object 이외의 타입을 매개변수로 받는 equals메서드는 재정의가 아니라 다중정의가 되는 것임으로 선언하지 말자!

핵심 정리

많은 경우에 Object의 equals가 우리가 원하는 비교를 정확히 수행해주지만 꼭 필요한 경우가 아니면 equals를 재정의하지 말자. 재정의해야 할 때는 해당 클래스의 핵심 필드를 빠짐없이, 다섯 가지 규약을 확실히 지켜가며 비교해야 한다.

Buy me a coffeeBuy me a coffee
Written by

@Seongwon

기술공유를 통해 새로운 가치 창조을 추구하는 백엔드 개발자 오성원입니다.
©SeongwonOh