-
equals는 일반 규약을 지켜 재정의 하라JAVA/Effective java 2021. 1. 11. 13:41
- equals 메서드는 재정의하기 쉬워 보이지만 자칫 잘못된 결과를 초래할 수 있다.
- 따라서 가장 쉬운 길은 아예 재정의 하지 않는 것이다. 그냥 두면 그 클래스의 인스턴스는 오직 자신과만 같게 된다.
따라서 다음 열거한 상황 중 하나에 해당되면 재정의하지 않는 것이 최선이다.
1. 각 인스턴스가 본질적으로 고유할 때
- 값을 표현하는 게 아닌 동작하는 개체를 표현하는 클래스, Thread 클래스는 하나의 좋은 예다.
2. '논리적 동치성'을 검사할 일이 없을 때
- Object에 기본 equals는 객체를 식별하기 위해 주소 값을 비교한다.
- 만일 Pattern에 equals나 String에 equals 등 '논리적 동치성'의 경우가 없을 경우 재정의 하지 않아도 된다.
3. 상위 클래스에 재정의한 equals가 하위 클래스에 들어맞을 때
- Set의 구현체들은 AbstractSet, List에 구현체들은 AbstractList,
- Map 구현체들은 AbstractMap 등 이들은 부모 클래스에 equals를 상속받아 그대로 쓰기에 재정의 할 필요가 없다.
4. 클래스가 private 이거나 package-private인 default이고 equals 메서드를 호출할 일이 없을 때
- throw new AssertionError() 등으로 호출을 금지 켜 위험을 회피할 수도 있다.
그렇다면 eqauls를 재정의해야 할 때는 언제일까?
- 객체 식별성이 아닌 논리적 동치성을 확인해야 하는데,
- 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의 되지 않았을 때이다.
- 주로 값 클래스들이 여기에 해당한다.
- Integer나 String처럼 값을 표현하는 클래스들은 객체가 같은지가 아닌 값이 같은지를 알고 싶어 한다.
- equals가 논리적 동치성을 확인하도록 정의해두면, 값을 비교할 수 있고 Map과 Set의 원소로 사용할 수 있게 된다.
- 물론 값 클래스라 해도 같은 인스턴스가 둘 이상 만들어지지 않음을 보장한다면 재정의 하지 않아도 되며 Enum이 여기에 해당된다.
규약
- 세상에 홀로 존재하는 클래스는 없다. 한 클래스의 인스턴스는 다른 곳으로 빈번히 전달된다.
- 그리고 컬렉션 클래스들을 포함해 수많은 클래스는 전달받은 객체가 eqauls 규약을 지킨다고 가정하고 동작한다.
- 규약들에 관하여 알아보자.
반사성
- 객체는 자신과 같아야 한다.
- 이 요건을 어긴다면 컬렉션에 넣은 다음 contains 메서드를 호출하면 인스턴스가 없다고 답할 것이다.
대칭성
- 서로에 동치 여부에 똑같이 답해야 한다. 다음의 예를 보자.
- 이 예는 s.equals(cis)의 결과와 cis.equals(s)의 결과가 다르다.
- 즉 cis String의 eqauls는 일반 String을 알아도 String의 equals는 cis의 존재를 모른다.
- 이는 명백히 대칭성을 위반한다.
- 따라서 cis의 equals를 String과도 연동하겠다는 허황된 꿈은 버려야 한다.
추의성
첫 번째 객체와 두 번째 겍체가 같고 두번째와 세번째 객체가 같다면 첫번째와 두번째 객체도 같아야 한다는 뜻이다.
- Point 클래스를 확장해서 점에 색상을 더해보자.
- equals 메서드는 어떻게 해야 할까?
- equals를 그대로 두게 되면 Point 구현이 상속되어 색상 정보는 무시한 채 비교한다.
- equals 규약은 어긴 것이 아니지만 중요한 정보를 놓치게 되니 받아들일 수 없다.
- 위치와 색상이 같을 때만 true를 반환하는 equals를 만들어보자.
- 이 메서드는 일반 Point를 ColorPoint에 비교한 결과와 그 둘을 바꿔 비교한 결과가 다를 수 있다.
- Point의 equals는 색상을 무시하고 ColorPoint의 equals는 입력 매개변수 클래스 종류가 다르기에 매번
- false를 반환하게 된다.
- p.equals(cp)는 true를 cp.equals(p)는 false를 반환한다.
그렇다면 ColorPoint.equals가 Point와 비교 시에는 색상을 무시하도록 하면 될까?
- 이 방식은 대칭성을 지켜주지만, 추이 성을 깨버린다.
- 각각 true, true, false를 반환한다. p1과 p3비교는 색상까지 고려했기 때문이다.
- 이 방식은 무한 재귀에 빠질 위험까지 도사린다.
그렇다면 해법은?
- 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
- 이번 equals는 같은 구현 클래스의 객체와 비교할 때만 true를 반환한다.
- 하지만 실제로 활용할 수는 없다.
- Point의 하위 클래스는 정의상 여전히 Point이므로 어디서든 Point로써 활용될 수 있어야 한다.
- 리스 코프 치환 원칙은 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 동작해야 한다.
- 따라서 그 타입의 모든 메서드가 하위 타입에서도 똑같이 잘 작동해야 한다.
- 그런데 CounterPoint의 인스턴스를 onUnitCircle 메서드에 넘기면 어떻게 될까?
- Point 클래스의 equals를 getClass를 사용해 작성했다면 false를 반환한다.
- Set을 포함한 대부분의 컬렉션은 equals 메서드를 이용하는데 CounterPoint의 인 스터 스는 어떤 Point와도 같을 수
- 없기 때문이다.
- 반면 equals를 instanceOf 기반으로 올바로 구현했다면 CounterPoint 인스턴스 역시 제대로 동작할 것이다.
- 구체 클래스의 하위 클래스에서 값을 추가할 방법은 없지만 괜찮은 우회 방법은 '상속 대신 컴포지션의 사용'이다.
- Point를 상속 대신 Point를 ColorPoint의 private 필드로 두고 ColorPoint와 같은 위치의 일반 Point를
- 반환하는 뷰 메서드를 public으로 추가하는 식이다.
일관성
- 두 객체가 같다면 영원히 같아야 한다는 뜻이다.
- 불변 객체는 한번 다르면 끝까지 달라야 한다. 클래스를 작성할 때는 불변 클래스로 만드는 게 나을지 숙고하자.
- 불변 클래스라면 equals가 한번 같다고 한 객체는 영원히 같아야 한다.
- 따라서 equals의 판단에 신뢰할 수 없는 자원이 끼어서는 안 된다.
equals 메서드의 구현을 정리해보자면
1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
2. instanceOf 연산자로 입력이 올바른 타입인지 확인한다.
3. 입력 객체와 자기 자신의 대응되는 '핵심'필드들이 모두 일치하는지 하나씩 검사한다.
- 다음에 따라 작성해본 PhoneNumber 클래스용 equals 메서드이다.
- 이때 Object 외의 타입을 매개변수로 받는 equals 메서드를 선언하지 말자.
- 타입을 구체적으로 명시한 equals는 해가 된다.
정리
- 꼭 필요한 경우가 아니면 eqauls를 재정의하지 말자.
- 많은 경우 Object의 equals가 원하는 비교를 정확히 수행해준다.
- 재정의할 때는 이러한 규약들을 비교해가며 꼭 지켜서 재정의 하자.
참고 자료
'JAVA > Effective java' 카테고리의 다른 글
toString을 항상 재정의하라 (0) 2021.01.19 equals를 재정의하려거든 hashCode도 재정의하라 (0) 2021.01.14 Try-Finally 대신 Try-with-Resource 사용하라 (0) 2021.01.04 Finalizer와 Cleaner는 피하라 (0) 2021.01.03 더이상 쓰지 않는 객체 레퍼런스는 없애자 + Weak Reference (0) 2021.01.02