꼭 필요한 경우가 아니면 equals를 재정의 하지 말자. 많은 경우에 Object의 equals가 프로그래머가 원하는 비교를 정확히 수행해 준다. 재정의 해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약을 확실히 지켜가며 비교해야 한다.
다음과 같은 경우에는 equals를 재정의 하지 말자
1. 각 인스턴스가 본질적으로 고유할 때 (ex 객체가 파일일 때 파일의 경로나 이름 등은 다른 객체와 같지 않고 고유하기 때문이 equals를 재정의 할 필요가 없다.)
2. 인스턴스의 논리적 동치성을 검사할 일이 없을 때
3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 들어맞을 때
4. 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없을 때
equals는 객체 식별성(두 객체가 물리적으로 같은가)이 아니라 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의 되지 않았을 때 재정의 한다.
주로 Integer와 String처럼 값을 표현하는 클래스를 말한다.
equals 메서드를 재정의 할때는 반드시 일반 규약을 따라야 한다.
1. 반사성(reflexivity) : null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.
객체는 자기 자신과 같아야 한다는 뜻
2. 대칭성(symmetry): null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다는 뜻이다.
다음 대소문자를 구별하지 않는 문자열을 구현한 클래스에서 equals를 구현한 코드를 살펴보자..
위 프로그램을 실행시키면 다음과 같은 결과가 출력된다.
CaseInsensitiveString의 equals는 일반 String을 알고 있지만 String의 equals는 CaseInsensitiveString의 존재를 모른다.
따라서 s.equals(cis)는 false를 반환하여, 대칭성을 명백히 위반한다.
CaseInsensitiveString을 List에 넣었을 때 list.contains(s) 또한 false를 반환한다. 이 처럼 equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.
이 문제를 해결하려면 CaseInsensitiveString의 equals를 String과도 연동하겠다는 생각은 버리고 아래처럼 간단하게 오버라이딩 하자.
3. 추이성(transitivity): null이 아닌 모든 참조 값 x, y, x에 대해, x.equals(y)가 true이고 y.equals(z)도 true면 x.equals(z)도 true다.
추이성은 첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체도 같아야 한다는 뜻이다.
x, y의 좌표를 가진 Point를 상속받아 color를 추가하는 클래스를 만들어보자.
이렇게 Point의 equals 메서드 x, y의 좌표만 가지고 동치성을 확인하는데 ColorPoint는 새로운 필드 color가 추가되었다. 그렇다면 equals 메서드는 어떻게 할까?
그대로 둔다면 Point의 구현이 상속되어 color 정보는 무시된 채 비교를 수행한다. 중요한 정보를 놓치고 비교를 하였으니 의미 없는 동작이다.
ColorPoint에 equals를 구혔했다. 만약 매개변수로 입력받은 Object가 일반 Point라면 색생을 무시하고 비교하고, ColorPoint라면 색상까지 비교한다.
위 코드의 실행결과는 다음과 같다.
추이성을 위배한것이다. p1과 p2, p2와 p3 비교에서는 색상을 무시했지만, p1과 p3 비교에서는 색상까지도 고려했기 때문이다.
구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다!
4. 일관성(consistency): null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환한다.
일관성은 두 객체가 같다면 어느 하나 혹은 두 객체 모두가 수정되지 않는 한 앞으로도 영원히 같아야 한다는 뜻이다.
가변 객체는 비교 시점에 따라 서로 다를 수도 같을 수도 있는 반면, 불변 객체는 한번 다르면 끝까지 달라야 한다.
만약 클래스를 불면으로 만들기로 했다면 equals가 한번 같다고 한 객체와는 영원히 같다고 답하고, 다르다고 한 객체와는 영원히 다르다고 답하도록 해야 한다.
클랙스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없느니 자원이 끼어들게 해서는 안된다.
과거 자바의 java.net.URL의 equals 메서드는 주어진 URL과 매핑된 호스트의 IP 주소를 이용해 비교한다.
이 방식의 문제점은 호스트의 이름을 IP 주소로 바꾸려면 네트워크를 통해야 하는데, 그 결과가 항상 같다고 보장할 수 없기 때문이다. 따라서 equals는 항시 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야 한다.
5. null 아님: null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.
모든 객체가 null과 같지 않아야 한다는 뜻이다. 의도하지 않았음에도 o.equals(null)이 true를 반환하는 상황은 상상하기 어렵지만, 실수로 NullPointerException을 던지는 코드는 흔할 수 있다.
따라서 수많은 클래스가 입력이 null인지를 확인해 자신을 보호한다.
여기서 명시적 null 검사와 묵시적 null 검사가 있는데 묵시적 검사를 살펴보자.
동치성 검사를 하려면 equals는 건네받은 객체를 적절히 형변환 후 필수 필드의 값을 알아내야 한다. 그러려면 형변환에 앞서 instanceof 연산자로 입력 매개변수가 올바른 타입인지 검사해야 한다. 따라서 입력이 null 이면 타입 확인 단계에서 false를 반환하기 때문에 null 검사를 명시적으로 하지 않아도 된다. 즉, 굳이 명시적으로 null이 아닌지 확인하는 코드를 작성하지 않아도 된다.
지금까지의 내용을 종합해서 양질의 equals 메서드 구현을 단계별로 정리해 보면
1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
자기 자신이면 true를 반환한다. 단순한 성능 최적화용으로, 비교 작업이 복잡한 상황일 때 값어치를 한다.
2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
이때 올바른 타입이란 equals가 정의된 클래스인 것이 보통이지만, 가끔 그 클래스가 구현한 특정 인터페이스가 될 수도 있다.
3. 입력을 올바른 타입으로 형변환한다.
앞서 instanceof 검사를 했기 때문에 문제없이 성공한다.
4. 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사한다.
float와 double은 특수한 부동소수 값등을 다뤄야 하기 때문에 각각 정적 메서드인 Float.compare(float, float)와 Double.compare(double, double)로 비교한다.
이 둘을 제외한 기본 타입 필드는 == 연산자로 비교하고, 참조 타입 필드는 각각의 equals 메서드로 비교한다.
배열 필드는 원소 각각을 비교 하되, 모든 원소가 핵심 필드라면 Arrays.equals 메서드들 중 하나를 사용하자.
null도 정상 값으로 취급하는 참조 타입 필드도 있는데, 이런 필드는 정적 메서드인 Objects.equals(Object, Object)로 비교해 NullPointerException 발생을 예방하자.
어떤 필드를 먼저 비교하느냐가 equals의 성능을 좌지우지하기도 하는데, 최상의 성능을 바란다면 다를 가능성이 더 크거나 비교하는 비용이 싼 필드를 먼저 비교하자.
위 규칙을 지켜 전형적인 equals 메서드의 예를 살펴보자
여기에 추가해야할 주의사항이 있는데
equals를 재정의할 땐 hashCode도 반드시 재정의 하자
이건 Item 11에서 다룰 예정입니다.
Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자
이 메서드는 Object.equals를 재정의한 게 아니라 다중정의 한 것이다.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] Item 12. toString을 항상 재정의하라 (0) | 2023.03.09 |
---|---|
[Effective Java] Item 11. equals를 재정의 하려거든 hashCode도 재정의 하라 (2) | 2023.03.08 |
[Effective Java] Item 9. try-finally 보다는 try-with-resources 를 사용하라 (0) | 2023.03.07 |
[Effective Java] Item 8. finalizer 와 cleaner 사용을 피하라 (0) | 2023.03.05 |
[Effective Java] Item 7. 다 쓴 객체 참조를 해제하라 (0) | 2023.03.04 |