1. 반사성 : null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.
2. 대칭성 : null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
3. 추이성 : null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y)가 true이고 y.equals(z)도 true면 x.equals(z)도 true다.
4. 일관성 : null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환함
5. null-아님 : null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.
4. 일관성
- 두 객체가 같다면 (어느 하나 혹은 두 객체 모두가 수정되지 않는 한) 앞으로도 같다
- 가변 객체는 비교 시점에 따라 서로 다를 수도 혹은 같을 수도 있는 반면
- 불변 객체는 한번 다르면 끝까지 달라야 한다
- 불변 클래스로 만들기 했다면 equals가 한번 같다고 하면 같고 다르다고 하면 다르다고 답하도록 해야 한다
- 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안된다
java.net.URL의 equals는 주어진 URL과 매핑된 호스트의 IP 주소를 이용해 비교한다
- 호스트 IP 주소로 바꾸려면 네트워크를 통해야 하는데, 그 결과가 항상 같다고 보장할 수 없다
- URL의 equals가 일반 규약을 어기게 하고, 실무에서도 종종 문제를 일으킨다
- URL의 equals를 이렇게 구현한 것은 커다란 실수로 이어진다
- equals는 항시 메모리에 존재하는 객체만을 사용한 결정적 계산만 수행해야 한다
5. null-아님
- 모든 객체가 null과 같지 않아야 한다
- 의도하지 않았음에도 o.equals(null)이 true를 반환하는 상황은 상상하기 어렵지만,
- NullPointerException을 던지는 코드는 흔하다
- 이 일반 규약은 이런 경우도 허용하지 않는다
// 명시적 null 검사 - 필요없다
@Override public boolean equals(Object o) {
if (o == null)
return false;
...
}
- 이런 검사는 필요하지 않다
- equals는 건네받은 객체를 적절히 형 변환한 후 필수 필드들의 값을 알아내야 한다
- 그러려면 형 변환에 앞서 instanceof 연산자로 입력 매개변수가 올바른 타입인지 검사해야 한다.
// 묵시적 null 검사 - 이쪽이 낫다
@Override public boolean equals(Object o) {
if (!(o instanceof MyType))
return false;
MyType mt = (MyType) o;
...
}
equals 메서드 구현 방법을 단계별로 정리하기
1. == 연산을 사용해 입력이 자기 자신의 참조인지 확인한다
2. instanceof 연산자로 입력이 올바른 타입인지 확인한다
3. 입력을 올바른 타입으로 형 변환한다
4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다
equals를 다 구현했다면 세가지만 자문해보자
1. 대칭적인가?
2. 추이 성인가?
3. 일관적인가?
코드 10-6 전형적인 equals 메서드의 예
// 코드 10-6 전형적인 equals 메서드의 예 (64쪽)
public final 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, 9999, "가입자 번호");
}
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;
}
// 나머지 코드는 생략 - hashCode 메서드는 꼭 필요하다(아이템 11)!
}
- equals를 재정의할 땐 hashCode도 반드시 재정의하자
- 너무 복잡하게 해결하려 들지 말자 (필드들의 동치 성만 검사해도 equals 규약을 지킬 수 있다)
- Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자
많은 프로그래머가 equals를 다음과 같이 작성해놓고 문제의 원인을 찾아 헤맨다
// 잘못된 예 - 입력 타입은 반드시 Object여야 한다!
public boolean equals(MyClass o){
...
}
- 입력 타입이 Object가 아니므로 재정의가 아니라 다중 정의한 것이다
- 이처럼 '타입을 구체적으로 명시한' equals는 오히려 해가 된다
다음 equals 메서드는 컴파일되지 않고, 무엇이 문제인지를 정확히 알려주는 오류 메시지를 보여준다
// 여전히 잘못된 예 - 컴파일되지 않음
@Override public boolean equals(MyClass o){
...
}
꼭 필요한 경우가 아니면 equals를 재정의하지 말자.
많은 경우에 Object의 equals가 원하는 비교를 정확히 수행해준다
재정의해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약을 확실히 지켜가며 비교하자
'Books > Effective-Java 3판' 카테고리의 다른 글
12. toString을 항상 재정의하라 (0) | 2021.10.03 |
---|---|
11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2021.10.02 |
10.equals는 일반 규약을 지켜 재정의하라(2) (0) | 2021.09.29 |
10.equals는 일반 규약을 지켜 재정의하라(1) (0) | 2021.09.28 |
9.try-finally보다는 try-with-resources를 사용하라 (0) | 2021.09.27 |