본문 바로가기

공부 기록/Java

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

equals 메서드는 재정의하지 않는 것이 최선

- 아래에 열거한 상황 중 하나에 해당한다면 재정의하지 않는 것이 최선이다.

 

1. 각 인스턴스가 본질적으로 고유하다. => 값을 표현하는 게 아니라 동작하는 개체를 표현하는 클래스가 여기에 해당한다.

2. 인스턴스의 '논리적 동치성(logical equality)'을 검사할 일이 없다.

3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.

4. 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.

- equals가 실수로라도 호출되는 걸 막고 싶다면 아래와 같이 구현하자.

@Override
public boolean equals(Object o) {
    throw new AssertionError();
}

equals를 재정의해야 할 때

- 객체 식별성(object identity; 두 객체가 물리적으로 같은가)이 아니라 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때 => 주로 값 클래스

- 값 클래스라 해도, 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스(아이템 1)라면 equals를 재정의하지 않아도 된다. Enum(아이템 34)도 여기에 해당한다.


equals 메서드를 재정의할 때 따라야 할 일반 규약

- Object 명세에 적혀 있다.

 

1. 반사성(reflexivity) : null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true다.

   - 객체는 자기 자신과 같아야 한다.

2. 대칭성(symmetry) : null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.

   - 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다.

3. 추이성(transitivity) :null이 아닌 모든 참조 값 x, y, z에 대해, x.equals(y)가 true이고 y.equals(z)도 true면 x.equals(z)도 true다.

   - 첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체도 같아야 한다.

4. 일관성(consistency) : null이 아닌 모든 참조 값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 true 또는 false를 반환한다.

   - 두 객체가 같다면 (어느 하나 혹은 두 객체 모두가 수정되지 않는 한) 앞으로도 영원히 같아야 한다.

   - 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안 된다.

5. Not null : null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.

   - 모든 객체가 null과 같지 않아야 한다.

 

묵시적 null 검사

@Override public boolean equals(Object o) {
    if (!(o instanceof MyType))
        return false;
    MyType mt = (MyType) o;
    
    ...
    
}

 

- equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.

- 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.

- 상위 클래스를 직접 인스턴스로 만드는 게 불가능하다면 위의 규약을 어기는 문제들은 일어나지 않는다.


양질의 equals 메서드 구현 방법

1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.

2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.

3. 입력을 올바른 타입으로 형변환한다.

4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.

 

- 기본 타입 필드는 == 연산자로 비교하고, 참조 타입 필드는 각각의 equals 메서드로, float와 double 필드는 각각 정적 메서드인 Float.compare(float, float)와 Double.compare(double, double)로 비교한다. Float.equals와 Double.equals 메서드는 오토박싱을 수반할 수 있어 성능상 좋지 않다.

- 때론 null 도 정상 값으로 취급하는 참조 타입 필드도 있다. => 정적 메서드인 Objects.equals(Object, Object)로 비교하자.

- 어떤 필드를 먼저 비교하느냐가 equals의 성능을 좌우하기도 한다.

- equals를 재정의할 땐 hashCode도 반드시 재정의하자.(아이템 11)


전형적인 equals 메서드의 예

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 ranghCheck(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를 재정의하지 말자. 재정의해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약을 확실히 지켜가며 비교해야 한다.