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

둥그냥·2022년 4월 3일
0

Effective Java 독서

목록 보기
2/15

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

재정의하지 않아야 하는 경우

문제를 회피하는 가장 쉬운 길은 아예 재정의 하지 않는 것이다

특히 아래의 상황 중 하나에 해당된다면 재정의 하지 않는 것이 최선이다

  • 각 인스턴스가 본질적로 고유하다

  • 인스턴스의 '논리적 동치성'을 검사할 일이 없다

    • ex) java.util.regex.Pattern은 equals를 재정의해서 Pattern의 인스턴스가 같은 정규식을 나타내는지를 검사한다. (논리적 동치성을 검사하는 경우)
  • 상위 클래스에서 재정의한 equals가 하위 클래스에서도 딱 들어맞는다.

  • 클래스가 private이거나 package-private이고 equlas 메서드를 호출할 일이 없다

재정의해야하는 경우

논리적 동치성을 확인해야 하는 경우

더불어 상위 클래스의 equlas가 논리적 동치성을 검사하도록 정의되지 않아 그 역할을 못해줄 경우에 해당되는 말이다.

예시1) 값 클래스

Integer나 String처럼 값을 표현하는 클래스

재정의를 해야 하는 이유
  • 프로그래머는 객체가 같은지가 아니라 값이 같은지를 알고 싶어 할 것이다.
  • Map의 키와 Set의 원소로 사용할 수 있게 된다.
재정의를 하지 않는 경우
  • 값이 같은 인스턴스가 둘 이상 만들어지지 않음이 보장되는 인스턴스 통제 클래스라면 equals를 재정의하지 않아도 된다
    • ex) Enum

equals를 재정의 할 때 따라야할 일반 규약

equals 메서드를 재정의할 때는 반드시 일반 규약을 따라야 한다.

object 명세에 적힌 규약

equals 메서드는 동치관계를 구현하며, 다음을 만족한다.

  • 반사성
  • 대칭성
  • 추이성
  • 일관성
  • null-아님

동치관계란?

  • 집합을 서로 같은 원소들로 이뤄진 부분집합으로 나누는 연산
    • 이 연산을 동치류라고 한다
  • equals 메서드가 쓸모있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환할 수 있어야 한다.

반사성

객체는 자기 자신과 같아야 한다.

어길 경우

  • 컬랙션에 넣고 contains 메서드를 호출하면 방금 넣은 인스턴스가 없다고 할 것이다

대칭성

두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다

A.equals(B)가 true라면 B.equals(A)도 true여야 한다.

추이성

첫 번째 객체와 두번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체도 같아야 한다

A.equals(B)가 true고
B.equals(C)가 true면
A.equals(C)도 true여야 한다.

어기게 되는 상황 (책의 57page부터 참고)

좌표를 저장하는 Point class (equals 재정의),
이를 상속한 좌표와 색상까지 저장하는 ColorPoint class가 있을 때
ColorPoint의 equlas도 재정의하다 보면 추이성을 깨게 된다.

  • 모든 객체 지향 언어의 동치관계에서 나타나는 근본적인 문제
  • 구체 클래스를 확장해 새로운 값을 추가하면서 equlas 규약을 만족시킬 방법은 존재하지 않는다.
  • 해결 방안 (우회 방안) 아이템 18을 참고

어기게 되었을 때의 예시

java.sql.Timestamp는 java.util.Date를 확장한 후 nanoseconds필드를 추가함

  • Timestamp의 equals는 대치엉을 위배하며 Date 객체와 섞어 사용하면 엉뚱하게 동작할 수 있다.
  • Timestamp를 이렇게 설계한 것은 실수니 절대 따라 해서는 안된다.

일관성

두 객체가 같다면 앞으로도 영원히 같아야 한다
(어느 한쪽도 수정되지 않는 한)

equlas의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안 된다.
equals는 항시 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야 한다.

어기게 되었을 때의 예시

java.net.URL의 equals는 주어진 URL과 매핑된 호스트의 IP주소를 이용해 비교한다.

  • 호스트 이름을 IP 주소로 바꾸려면 네트어크를 통해야 함
    • 이는 항상 같은 결과를 보장하지 않는다.
  • 이는 URL의 equals가 일반 규약을 어기게하고 실무에서도 종종 문제를 일으킨다.
  • URL의 equals를 이렇게 구현한 것은 커다란 실수였으니 절대 따라 해서는 안 된다.
    • 하위 호환성이 발목을 잡아 잘못된 동작을 바로 잡을 수도 없다.

null-아님

공식 이름은 아니다.
모든 객체가 null과 같지 않아야 한다.

o.equals(null)이 true를 반환해서는 안된다.
o.equals(null)이 NullPointerException을 던져서도 안된다. (종종 실수로 발생한다)

좋은 예시

// 묵시적 null 검사
@Override public boolean equals(Object o){
	if(!(o instanceof MyType))
    	return false;
    MyType mt = (MyType) o;
    ...
}
  • 동치성을 검사하려면 어차피 형변환이 필요, 이를 위해서 instanceof 연산자로 타입을 검사하게 된다.

나쁜 예시

// 명시적 null 검사
@Override public boolean equals(Object o){
	if(o == null)
    	return false;
    ...
}
  • 이러한 검사는 필요하지 않다.

좋은 equals 메서드 구현 단계

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

equals 구현 후 확인해야 할 것

  • equals를 다 구현했다면 세 가지를 자문해보자
    1. 대칭적인가?
    2. 추이성이 있는가?
    3. 일관적인가?
  • 자문 후에는 단위 테스트를 돌려보자 (단 AutoValue를 이용해 작성했다면 테스트 생략 가능)

전형적인 equals 메서드의 예

이상의 비법에 따라 작성된 이상적인 equals 메서드의 예시

public final class PhoneNumber{
	private final short areaCode, prefix, lineNum;
   
    ... 생략
    
    @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를 재정의할 땐 hashCode도 반드시 재정의하자(아이템 11)
  • 별칭은 비교하지 않는 게 좋다.
    • ex) 만약 File 클래스라면 심볼릭 링크를 비교해 같은 파일을 가리키는지 확인하려 들면 안된다
  • Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자

📌 핵심 정리

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

0개의 댓글