■ equals를 재정의 하지 않는게 좋은 상황
- 각 인스턴스가 본질적으로 고유할 때
- 값 클래스가 아닌 동작하는 개체를 표현하는 클래스 (Bean 해당)
- 인스턴스의 논리적 동치성을 검사할 일이 없을 때
- 자바 Pattern은 equals를 재정의 하여도 Pattern의 정규 표현식을 비교
- 상위 클래스에 재정의한 equals가 하위 클래스에 맞을 때
- Set은 AbstractSet이 구현한 equals를 상속. List는 AbstractList, Map은 AbstractMap의 equals를 상속한다.
- 클래스가 private나 package-private이고 equals를 호출할 일이 없을 때
■ equals를 재정의 해야 하는 경우
두 객체의 주소가 같은지 비교해야하는게 아니라 '논리적 동치성'을 확인해야하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의 되지 않았을 경우(주로 값 클래스 : Integer, String) 두 값 객체를 equals로 비교하는 경우, 객체가 같은지가 아니라 값이 같은지를 검사하고 싶은 것임.
■ equals 메서드 재정의 일반 규약 : 동치 관계
-
반사성(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다.
//SmellPoint.java의 equals
@Override public boolean equals(Obejct o){
if(!(o instanceof Point))
return false;
if(!(o instanceof SmellPoint))
return o.equals(this);
return super.equals(o) && ((SmellPoint) o).color == color;
}
public static void main(){
ColorPoint p1 = new ColorPoint(1,2, Color.RED);
SmellPoint p2 = new SmellPoint(1,2);
p1.equals(p2);
// 1. ColorPoint의 equals: 2번째 if문 때문에 SmellPoint의 equals로 비교
// 2. SmellPoint의 equals: 2번째 if문 때문에 ColorPoint의 equals로 비교
// 3. 1~2 무한 재귀로 인한 StackOverflow Error
}
■ equals 메서드 구현 방법
- "=="연산자를 사용해 입력이 자기 자신의 참조인지 확인한다. 자기 자신이면 true를 반환한다. 단순한 성능 최적화용으로 비교 작업이 복잡한 상황일 때 값어치를 한다.
- instanceof 연산자로 입력이 올바른 타입인지 확인한다. 가끔 해당 클래스가 구현한 특정 인터페이스를 비교할 수도 있다. 이런 인터페이스를 구현한 클래스라면 equals에서 (클래스가 아닌) 해당 인터페이스를 사용해야한다.
ex) Set, List, Map, Map.Entry 등 컬렉션 인터페이스들 - 입력을 올바른 타입으로 형변환 한다. 2번에서 instanceof 연산자로 입력이 올바른 타입인지 검사 했기 때문에 이 단계는 100% 성공한다.
- 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
모두 일치해야 true를 반환한다.
■ equals 메서드 구현 시 주의 사항
- 기본 타입 : "=="연산자 비교
- 참조 타입 : equals 메서드로 비교
- float, double 필드 : 정적 메서드 Float.compare(float, float)와 Double.compare(double, double)로 비교
Float.equals(float)나 Double.equals(double)은 오토 박싱을 수반해 성능상 좋지 않다. - 배열 필드 : 원소 각각을 지침대로 비교. 모두가 핵심 필드라면 Arrays.equals() 사용
- null 정상값 취급 방지 : Object.equals(object, object)로 비교하여 NullPointException 발생을 예방한다.
- 비교하기 복잡한 필드를 가진 클래스 : 필드의 표준형(canonical form)을 저장한 후 표준형끼리 비교
- 필드의 비교 순서는 equals 성능을 좌우한다. -> 다를 가능성이 크거나 비교하는 비용이 싼 필드부터 비교. 파생 필드가 객체 전체 상태를 대표하는 경우, 파생 필드부터 비교.
- **equals를 재정의할 땐 hashCode도 반드시 재정의하자**
- Object 외의 타입을 매개변수로 받는 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, 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;
}
}
■ 결론
꼭 필요한 경우가 아니면 equals를 재정의하지 말자. 많은 경우에 Object의 equals가 여러분이 원하는 비교를 정확히 수행해준다. 재정의해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약을 확실히 지켜가며 비교해야 한다.
REFERENCE
'Effective Java' 카테고리의 다른 글
[Effective Java] 아이템11 equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2021.01.24 |
---|---|
[Effective Java] 아이템9 try-finally 보다는 try-with-resources를 사용하라 (0) | 2021.01.24 |
[Effective Java] 아이템8 finalizer와 cleaner 사용을 피하라 (0) | 2021.01.21 |
[Effective Java] 아이템7 다 쓴 객체 참조를 해제하라 (0) | 2021.01.17 |
[Effective Java] 아이템6 불필요한 객체 생성을 피하라 (0) | 2021.01.16 |