EJ2E Item 9. equals를 재정의할 땐 hashCode도 재정의하라.

참조: Effective Java 2nd Edition Item 9. Always override hashCode when you override equals

equals를 재정의한 클래스는 반드시 hashCode를 재정의 해야 한다. 그렇지 않으면 Object.hashCode 일반적인 계약을 위반하게 된다. 이를 위반하면 hash 기반의 컬렉션(HashMap, HashSet, Hashtable)에서 제대로 동작하지 않을 것이다.

JavaSE6 Object 표준

  • hashCode를 여러번 호출 할 때마다 같은 integer를 반환해야 한다.
  • 두 객체가 equals(Object) 메소드로 동일할 때, hashCode도 같은 integer를 반환해야 한다.
  • 필수는 아니지만, equals(Object)로 가지 않은 두 객체는 hashCode를 호출했을 때 반드시 반드시 다른 integer를 반환해야 한다.

이 중에서 핵심은 두 번째 것.

HashMap에서 key로 사용하는 객체의 hashCode 값이 다르면 equals로 같은 객체여도 같은 key로 인식하지 않는다.

좋은 hashCode 만드는 방법

  • result 라는 int 변수에 0이 아닌 수를 넣는다.
  • 각각의 필드에 다음과 같은 계산식을 적용한다.
    • 필드가 boolean이면, (f ? 1 : 0)
    • 필드가 byte, char, short, int면, (int) f
    • 필드가 long이면, (int)(f ^ (f >>> 32))
    • 필드가 float면, Float.floatToIntBits(f)
    • double이면, Double.doubleToLongBits(f)를 한 다음에 long 타입 다루듯이 한 번 더 계산
    • 레퍼런스 타입이면 hashCode 호출하여 그 결과값 반환. null이면 0 반환
  • result = 31 * result + 위에서 계산한 값
  • result를 반환한다.
  • 작성후 equals로 같은 객체가 같은 hashCode를 반환하는지 단위 테스트로 검증.

equals에서 사용하지 않는 필드는 hashCode에서도 사용하면 안 된다.

example

@Override public int hashCode() {
  int result = 17;
  result = 31 * result + areaCode;
  result = 31 * result + prefix;
  result = 31 * result + lineNumber;
  return result;
}

EJ2E Item 8. equals를 재정의 할 떄는 일반적인 계약을 따르라.

참조: Effective Java 2nd Edition Item 8. Obey the general contract when overriding equals

equals 메소드 재정의은 간단해 보이지만 잘못 될 여지가 많다.

equals 메소드 재정의가 필요 없는 경우

  • 클래스 특성상 각각의 객체가 유일할 때. ex) Thread
  • “논리적인 일치” 확인 기능을 제공하는지 관심 없을 때. ex) Random
  • 이미 상위 클래스에서 재정의한 equals를 재공하며, 그 로직이 현재 클래스서도 적당할 때. ex) AbstractSet, AbstractList, AbstractMap
  • 클래스가 private 또는 package-private인 경우 equals가 절대로 호출되지 않을거라는 확신이 있을 때.

equals 메소드 재정의가 필요한 경우

  • logical equality 개념이 있는 클래스
  • 보통 value class(예외, 싱글톤, Enum 타입 – Object의 equals가 곧 logical equality)
  • ex) Integer, Date

equals를 재정의할 때 따라야 하는 일반적인 계약(JavaSE6 Object 스펙)

  • Reflexive: null이 아닌 레퍼런스 값 x에 대해, x.equals(x)는 반드시 true를 반환해야 한다.
  • Symmetric: null이 아닌 레퍼런스 값 x와 y에 대해, y.equals(x)가 true를 반환 경우에 한 해서만 x.equals(y)도 true를 반환해야 한다.
  • Transitive: null이 아닌 레퍼런스 값 x, y, z에 대해, x.equals(y)가 true고 y.equals(z)가 true면 x.equals(z)도 반드시 true여야 한다.
  • Consistent: null이 나닌 레퍼런스 값 x와 y에 대해, x.equals(y)를 몇 번 호출하든지 계속해서 일관적으로 true를 반환하거나 false를 반환해야 한다.
  • null이 아닌 레퍼런스 값 x에 대해, x.equals(null)은 반드시 false를 반환한다

규칙을 어기면 다른 객체가 어떻게 동작할지 예측하기 힘들다.

고품질 equals 메소드 레서피

  • 같은 객체를 참조하는 레퍼런스가 아닌지 확인. == 사용.
  • 정당한 타입인지 확인할 떄는 instanceof 연산자를 사용,
  • 적당한 타입으로 캐스팅
  • 각각의 필드가 같은지 확인. primitive 타입은 == 사용, 레퍼런스 타입은 equals 사용
  • 메소드 작성을 마친 뒤에, symmetric, transitive, comsistent 한지 단위 테스트를 작성한다.

example

@Override public boolean equals(Object o) {
  if (o == this)
    return true;
  if (!(o instanceof PhoneNumber))
    return false;
  PhoneNumber pn = (PhoneNumber)o;
    return pn.lineNumber == lineNumber
      && pn.prefix  == prefix
      && pn.areaCode  == areaCode;
}

연변 말투 == 보이스피싱

방금 전 사무실로 보이스피싱 전화 한 통이 걸려왔습니다. 보이스피싱 인지 아닌지는 대충 몇 마디만 대화를 나누면 확신이 서고 전화를 끊게 만듭니다.

“여보세요 에스엘티입니다.”

“…”

“어디세요?”

“식약청인데요.”  // 일단 말투가 연변 말투인데다가, 주춤 거리는걸 보고 50% 확신

“네? 식약청이요? 왜요?”

“누가 연락을 주셨는데요.” // 그럴 사람이 회사 내에 없음으로. 80% 확신.

“누가 무슨 연락을 주셨는데요?”

“아.. 그럼 핸드폰 번호를..” // 전혀 어이없는 플로우로 개인 정보 요구. 99% 확신

‘찰칵’ // 이쯤되면 끊어도 이상하지 않습니다.

끊어 버린뒤 현재 5분 정도 경과 됐는데 연락이 안 오는거 보면 100% 보이스피싱.

말투에서 일단 50% 먹고 들어가는 연변 알바야. 제발 다른 일 좀 하면 안 되겠니?