Effective Java - item10-12

2 minute read

[effective java] 3장 모든 객체의 공통 메서드

Object를 생성할 때 final이 아닌 equals, hashCode, toString, finalize와 같은 메서드들은 오버라이딩해야 한다.

item10. equals는 일반 규약을 지켜 재정의하라

Equals 메서드는 아래의 경우 재정의하지 말자.

  • 각 인스턴스가 본질적으로 고유하다. 값이 아니라 동작하는 개체를 표현하는 클래스인 경우(ex. Thread)
  • 인스턴스의 논리적 동치성을 검사할 일이 없다.
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 적용된다.
  • 클래스가 private으로 선언되고 equals 메서드를 사용할 일이 없다.
  • 값이 같은 인스턴스가 둘 이상 만들어지지 않는 것이 보장된 경우(ex. Enum)

Equals 메서드는 Integer, String과 같이 값을 표현하는 클래스인 경우에 객체가 아닌 값이 같은지 보기 위해 사용하는 것이다.

Equals 재정의 시 따라야 하는 규약은 아래와 같다.

  • 반사성: null이 아닌 모든 참조값 x에 대해 x.equals(x) = true

  • 대칭성: null이 아닌 모든 참조값 x, y에 대해 x.equals(y)=true면 y.equals(x)=true

  • 추이성: null이 아닌 모든 참조값 x, y, z에 대해 x.equals(y)=true고 y.equals(z)=true면 x.equals(z)=true

    하위 클래스에 새로운 값을 추가하면서 equals 규약을 만족시킬 방안은 없다.

  • 일관성: null이 아닌 모든 참조값 x, y에 대해 x.equals(y)를 여러번 호출하면 항상 같은 값

    equals의 판단에 신뢰할 수 없는 자원이 끼어있으면 안된다.

  • null-아님: null이 아닌 모든 참조값 x에 대해 x.equals(null)=false

이렇게 규약을 지킨 equals 메서드의 구현 방법은 아래와 같다.

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

값 비교 시 Float.equals와 같은 메서드는 오토박싱이 동반되어 성능상 좋지 않으니 == 연산자로 비교하도록 해야한다.

어느 필드를 먼저 비교하느냐에 따라 성능이 좌우되기도 하니 비용이 싼 필드를 먼저 비교하도록 하자.

동기화용 락 필드 처럼 객체의 논리적인 상태와 관련 없는 필드는 비교하면 안된다.

요약해서 equals를 사용할 때 주의점은 아래와 같다.

  • 대칭적인가? 추이성이 있는가? 일관적인가?
  • equals를 재정의할 땐 hashCode도 재정의하자
  • 너무 복잡하게 해결하려 하지 말자. 별칭 비교도 주의하자
  • Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자.

item11. equals를 재정의하려거든 hashCode도 재정의하라.

hashCode의 일반 규약을 어기면 HashMap, HashSet 같은 컬렉션의 원소로 사용할 때 문제가 될 수 있다.

hashCode의 일반 규약은 아래와 같다.

  • equals에 사용되는 정보가 그대로면 hashCode도 동일한 값을 반환해야한다.
  • equals가 true면 hashCode도 같은 값을 반환해야한다.
  • equals가 false더라도 hashCode는 같을 수 있다. 하지만 달라야 성능이 좋아진다.

예를 들어 hashMap에서 같은 버킷을 사용했는데도 get을 했을 때 hashCode가 달라지면 문제가 발생할 수 있다.

hashCode 메서드를 작성하는 방법은 아래와 같다.

  • int 변수 result를 선언한 후 값 c로 초기화한다. 이 때 c는 객체의 첫번째 핵심 필드를 2.a 방식으로 계산한 해시코드
  • 해당 객체의 나머지 핵심필드 각각에 대해 아래 작업을 수행해 필드의 해시코드 c를 계산해 result를 31*result+c로 반환한다.
    • 기본 타입 필드라면 Type.hashCode 수행, Type은 박싱 클래스
    • 참조 타입 필드면서 equals 메서드가 equals를 호출하면 hashCode를 재귀적으로 함께 호출한다.
    • 필드가 배열이면 원소 각각을 별도 필드처럼 다룬다.

핵심 필드는 반드시 포함해야하고, 파생 필드는 선택적으로 포함하지 않아도 되고, equals에 사용되지 않은 필드는 반드시 제외해야 한다.

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

//제공되는 메서드, 하지만 느림 -> 배열 만들고 박싱 언박싱도 거치기 때문
@Override
public int hashCode() {
  return Objects.hash(lineNum, prefix, areaCode);
}

클래스가 불변이고 해시코드를 계산하는 비용이 크다면 캐싱을 고려하면 좋다.

뿐만 아니라 hashCode가 반환하는 값의 생성 규칙을 클라이언트에 자세히 공표하지 않아야 클라이언트가 값에 의지하지 않고 계산 방식도 바꿀 수 있다.

item12. toString을 항상 재정의하라

Object가 제공하는 toString 메서드는 단순히 클래스 이름@16진수로 표시한 해시코드일 뿐이다.

toString을 잘 구현한 클래스는 사 용하기 쉽고, 클래스를 사용한 시스템은 디버깅하기 쉽다.

따라서 재정의를 통해 객체가 가진 주요 정보를 모두 반환하는 것이 좋다.

반환값의 포맷을 문서화할지, 어떻게 명시할지 등을 잘 정하고 의도를 명확히 밝혀야 한다.

정적 유틸 클래스는 toString이 불필요하고, enum은 이미 완벽한 toString을 제공하니 재정의가 불필요하다. 하지만 추상클래스는 재정의가 필요하다.

Tags:

Categories:

Updated: