[Java][Effective Java] item 10. equals는 일반 규약을 지켜 재정의하라 (일관성)

2022. 6. 7. 21:37JAVA/Effective Java

1. equals 메서드 재정의시 지켜야 하는 일반 규약 : 일관성(consistency)

일관성은 두 객체가 같다면(어느 하나 혹은 두 객체 모두가 수정되지 않는 한) 앞으로도 영원히 같아야 한다는 의미입니다. 가변 객체(mutable object)는 비교 시점에 따라 서로 다를수도 혹은 같을 수도 있는 반면, 불변 객체(immutable object)는 한번 다르면 끝까지 달라야 합니다. 따라서 불변 클래스로 만들기로 했다면 equals가 한번 같다고 한 객체와는 영원히 같다고 답하고, 다르다고 한 객체와는 영원히 다르다고 답하도록 만들어야 합니다.

 

일관성 조건을 만족시키지 않는 조건

  • 클래스가 불변 또는 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어드는 경우

예를 들어 java.net.URL의 equals는 주어진 URL과 매핑된 호스트의 IP 주소를 이용해 비교합니다. 호스트 이름을 IP 주소로 변경하려면 네트워크를 통해야 하는데, 그 결과가 항상 같다고 보장할 수 없습니다. 이는 URL의 equals가 일관성 규약을 어기게 하고 문제를 일으킬 것입니다.

 

위와 같은 문제를 피하려면 equals는 항상 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야 합니다.

 

2. equals 메서드 재정의시 지켜야 하는 일반 규약 : Null-아님

Null-아님 일반 규약은 이름처럼 모든 객체가 null과 같지 않아야 한다는 의미입니다. 객체가 null과 같지 않아야 하는 경우는 대표적인 예외인 NullPointerException 예외도 포함됩니다. 

 

논리적 동치성을 검사하는 과정에서 instanceof 연산자를 사용하면 묵시적으로 매개변수로 받은 객체가 Null인지 검사할 수 있습니다.

// 묵시적 null 검사
@Override
public boolean equals(Object o){
    if(!(o instanceof MyType)){
    	return false;
    }
    MyType mt = (MyType) o;
    ...
}
  • null instanceof MyType 결과 : false

3. equals 메서드 재정의 단계별 정리

1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인합니다.

@Override
public boolean equals(Object o) {
	if (o == this) {
		return true;
	}
}

2. instanceof 연산자로 입력이 올바른 타입인지 확인합니다.

@Override
public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (o instanceof Product) {

    }
}

3. 입력을 올바른 타입으로 형변환한다.

3번 단계는 앞선 2번 단계의 instanceof 검사를 통과했기 때문에 100% 성공합니다.

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (o instanceof Product) {
      Product that = (Product) o;
    }
  }

4. 입력 객체(Object o)와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사합니다. 모든 필드가 일치하면 true를 반환하고 아니라면 false를 반환합니다.

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (o instanceof Product) {
      Product that = (Product) o;
      return (this.name.equals(that.name()))
           && (this.price.equals(that.price()));
    }
    return false;
  }
  • 성능을 높이기 위해서 필드중 다를 가능성이 더 크거나 비교하는 비용이 싼(혹은 둘다 해당하는) 필드를 먼저 비교합니다.

위 4단계를 다 구현했다면 equals 메소드가 대칭적인지 추이성이 있는지 일관성이 있는지 검사합니다.

 

다음은 위 4단계를 기반으로 구현된 equals 메서드입니다.

public class PhoneNumber {
	private final short areaCode, prefix, lineNum;

	public PhoneNumber(int areaCode, int prefix, int lineNum) {
		this.areaCode = rangeCheck(areaCode, 999, "지역코드");
		this.prefix = rangeCheck(prefix, 999, "프리픽스");
		this.lineNum = rangeCheck(lineNum, 999, "가입자번호");
	}
	
	private static short rangeCheck(int val, int max, String arg) {
		if(val < 0 || val > max) {
			throw new IllegalArgumentException(arg + ": " + val);
		}
		return (short) val;
	}

	@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도 재정의 해주어야 한다.
	@Override
	public int hashCode() {
		return Objects.hash(areaCode, prefix, lineNum);
	}	
}

위 예제에서 주목할점은 hashCode 메서드도 재정의 했다는 점입니다. 이는 equals 메서드가 재정의 되었다는 의미는 비교 기준이 변경되었다는 의미이기 때문에 Object.hashCode 메서드로는 비교할 수 없기 때문입니다. 따라서 hashCode 메서드도 재정의하여 비교 기준을 맞추어 줍니다.

 

핵심 정리

꼭 필요한 경우가 아니라면 equals를 재정의하지 말자. 많은 경우에 Object의 equals가 원하는 비교를 정확히 수행해줍니다. 재정의해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약, 반사성, 대칭성, 추이성, 일관성, Null-아님을 확실히 지켜가며 비교해야 합니다.

 

References

source code : https://github.com/yonghwankim-dev/effective_java/tree/master/src/role10
effective java 3/E