본문 바로가기
Books/Effective-Java 3판

10.equals는 일반 규약을 지켜 재정의하라(3)

by 두두리안 2021. 10. 1.
728x90
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가 원하는 비교를 정확히 수행해준다
재정의해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약을 확실히 지켜가며 비교하자
728x90