아이템10. equals는 일반 규약을 지켜 재정의하라
2021-06-02 00:00:00
# Effective_Java
- 재정의 하기 쉬워보이지만 곳곳에 함정이 있다.
차라리 재정의하지 않는 것이 나은 상황
- 각 인스턴스가 본질적으로 고유하다.
- 값을 표현하는 것이 아닌 동작하는 개체를 표현하는 클래스 ex) Thread
- 인스턴스의 ‘논리적 동치성’을 설계자가 검사할 일이 없을 때
- 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞을 때
- 클래스가 private 이거나 package-private 이고 equals 메서드를 호출할 일이 없다.
- (추가) 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스일 때 ex) Enum
언제 재정의해야하나?
- 객체 식별성이 아닌 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때
- 주로 값 클래스들이 해당된다. ex) Integer, String
- 두 값 객체를 equals로 비교하려는 프로그래머는 객체가 같은 것을 확인하는 것이 아ㅏ니라 값이 같은지를 알고 싶어 할 것이다.
equals()를 재정의할 때 따라야 할 일반 규약 - Object 명세
equals()는 동치관계(equivalence relation)를 구현하며, 다음을 만족한다.
반사성(reflexivity): null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.
대칭성(symmetry): null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
추이성(transitivity): null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true이고 y.equals(z)도 true면, x.equals(z)도 true다.
일관성(consistency): null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
null-아님: null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.
- 동치관계: 집합을 서로 같은 원소들로 이뤄진 부분집합(동치 클래스, equivalence class)으로 나누는 연산
- equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환할 수 있어야 한다.
반사성
- 객체는 자기 자신과 같아야 한다.
대칭성
- 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다.
1 | import java.util.Objects; |
- 대칭성 위반: CaseInsensitiveString의 equals()는 String을 알고 있지만, String의 equals()는 CaseInsensitiveString의 존재를 모른다!
- equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.
1 | // 책에서 표현한 간단한 equals() |
1 | import java.util.Objects; |
추이성(1)
- 삼단논법의 얘기와 동일함
- 구체 클래스를 확장해 서로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
- 부모 클래스의 성질을 유지하면서 equals() 사용이 어렵다.
- 교재 예시 요약
- x, y 값을 가지는 Point 객체를 상속하는 ColorPoint 객체에는 color 정보가 추가로 있다.
- equals()를 사용하여 x, y, color 값이 모두 일치하는지 확인하고 싶다.
- ColorPoint를 재정의한다고 해도 Point에서는 ColorPoint를 알지 못한다.
- ColorPoint에서 Point 객체를 무시하면 대칭성은 지켜지지만 추이성이 위배된다.
- 심지어 무한재귀를 발생할 수 있다.
- instanceof를 getClass 검사로 바꾸면? -> 리스코프 치환 원칙 위배
- Point의 하위 클래스가 어디서든 Point로 활용될 수 있어야하는데 그렇지 못한 상황이 생긴다.
추이성(2) 대안 - Composition
- (역시 상속은 너무 단점이 많다.)
- Point를 상속하는 대신 Point를 ColorPoint의 private 필드로 두고 구현
1 | import java.util.Objects; |
- 추상 클래스의 하위 클래스라면 equals 규약을 지키면서 값을 추가 가능하다.
- 상위 클래스를 직접 인스턴스로 만들 수 없다면 가능하다.
일관성
- 두 객체가 같다면 앞으로도 영원히 같아야 한다.
- equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안 된다.
- URL의 equals()가 URL과 매핑된 호스트의 IP주소를 비교하는데, 호스트 이름을 IP 주소로 바꾸기 위해 네트워크를 통할 때 그 결과가 항상 같다고 보장할 수 없다.
- URL의 사례와 같은 문제를 피하려면 equals()는 항시 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야 한다.
null-아님
- 모든 객체가 null과 같지 않아야 한다.
- equals()로 들어온 객체가 null인지 아닌지 검사할 필요 없다.
- instanceof를 사용하는 equals()에서는 타입 검사에서 false를 반환하므로 필요없다.
- (추가) IntelliJ에서 equals() 오버라이드 시에는 null 확인과 해당 객체의 클래스가 현재 객체와 동일한 클래스인지 확인하고 있다.
양질의 equals 메서드 구현 방법
- == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
- instanceof 연산자로 입력이 올바른 타입인지 확인한다.
- 입력을 올바른 타입으로 형변환한다.
- 입력 객체와 자기 자신의 대응되는 ‘핵심’ 필드들이 ㅁ두 일치하는지 하나씩 검사한다.
그래서 구현한 equals()가 대칭적이고 추이성이 있고, 일관적인가?
- Unit Test를 작성해서 돌려보자
- 3가지 요건 중 하나라도 실패한다면 원인을 찾아 고치자 (이 3가지가 주요 문제이기 때문이다.)
- equals를 재정의할 때 hashCode도 반드시 재정의하자 (아이템11) (TDD 책에서도 얘기했던 부분)
- 너무 복잡하게 해결하려고 파고들다가 오히려 실수가 발생한다.
핵심 정리
- 꼭 필요한 경우가 아니면 equals를 재정의하지 말자.
- 재정의해야 할 때에는 그 클래스의 핵심 필드 모두를 빠짐없이 5가지 규약을 확실히 지켜가며 비교해야 한다.