아이템10. equals는 일반 규약을 지켜 재정의하라

2021. 7. 2. 19:00책/이펙티브자바

아래에 상황 중 하나에 해당한다면 equals 는 재정의 하지 않는 것이 최선이다.

  1. 각 인스턴스가 본질적으로 고유하다.
    • 값을 표현하는게 아니라 동작하는 개체를 표현하는 클래스
    • ex) Thread
  2. 인스턴스의 '논리적 동치성'을 검사할 일이 없다.
  3. 상위 클래스에서 재정의한 equals 가 하위 클래스에도 딱 들어맞는다.
    • 예를들어 대부분의 Set, List 구현체들은 AbstractSet, AbstractList가 구현한 equals 를 그대로 사용한다.
  4. 클래스가 private 이거나 package-private이고 equals 메소드를 호출할 일이 없다.
// euqals가 실수로라도 호출되는걸 막고싶을 때
@override
public boolean equals(Obejct o) {
    throw new AssertionError();
}

그렇다면 언제 equals() 를 재정의 해야 하는가 ?

객체 식별성(object identity: 두 객체가 물리적으로 같은가) 이 아니라 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의 되지 않았을 때이다.

주로 Integer, String 과 같은 값 클래스들이 이에 해당한다.

equals() 를 재정의할 때 반드시 따라야하는 일반 규약

  • 반사성 (reflexivity)
    • null이 아닌 모든 참조 값 x에 대해, x.eqauls(x) = true
  • 대칭성 (symmetry)
    • null이 아닌 모든 참조 값 x, y에 대해, x.eqauls(y) 가 true 이면 y.equals(x) 도 true 이다.
  • 추이성 (transitivity)
    • null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y) 가 true이고 y.eqauls(z) 도 true이면 x.equals(z) 도 true 이다.
  • 일관성 (consisteny)
    • null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 같은 결과를 return 한다.
  • null-아님
    • null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false이다.

양질의 equals() 를 구현하는 방법

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

주의사항

  • 참조 타입 필드는 각각의 equals 로 비교
  • float과 double을 제외한 기본 타입 필드는 == 연산자로 비교
  • float과 double은 Float.compare, Double.compare 로 비교한다.
  • 배열의 모든 원소가 핵심 필드라면 Arrays.equals 메서드 들 중 하나를 사용하자.
  • null 비교는 Objects.equals() 로 비교해 NullPointerException 을 예방하자.
  • 최상의 성능을 바란다면 다를 가능성이 더 크거나 비교하는 비용이 싼 필드를 먼저 비교하자.
  • equals 재정의 할땐 hashCode도 반드시 재정의하자.
  • 너무 복잡하게 해결하려 들지 말자.
  • 매개변수로 Object 외의 타입을 받지말자.

equals()를 구현했다면 다음 사항을 확인해보자.

  • 확인만 하지 말고 단위테스트를 작성해보자.
  • 세 요건 중 하나라도 실패한다면 원인을 찾아서 수정하자.
  1. 대칭적인가 ?
  2. 추이성이 있는가 ?
  3. 일관적인가 ?

전형적인 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() 재정의는 하지말자.
  • 대부분의 경우에서는 Obejct.equals()가 원하는 비교를 수행해준다.
  • 그럼에도 재정의를 해야한다면 해당 클래스의 핵심 필드 모두를 빠짐없이, 다섯가지 규약을 확실히 지켜가며 비교한다.