본문 바로가기
Effective Java

[Effective Java] 아이템10 equals는 일반 규약을 지켜 재정의하라

by byeongoo 2021. 1. 22.

■ 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

velog.io/@lychee/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C-10.-equals%EB%8A%94-%EC%9D%BC%EB%B0%98-%EA%B7%9C%EC%95%BD%EC%9D%84-%EC%A7%80%EC%BC%9C-%EC%9E%AC%EC%A0%95%EC%9D%98-%ED%95%98%EB%9D%BC

 

[이펙티브 자바] 아이템 10. equals는 일반 규약을 지켜 재정의 하라

equals를 다 구현했다면 세 가지만 자문해보자.대칭적인가? 추이성이 있는가? 일관적인가?equals 메서드를 재정의하지 않고 그냥 두면, 그 클래스의 인스턴스는 오직 자기 자신과만 같게 된다. 각

velog.io