#08 equals 메서드를 오버 라이딩할 때는 보편적 계약을 따르자
Object는 실체 클래스(concrete class)이지만, 모든 클래스의 슈퍼 클래스로서 원래 상속을 목적으로 설계되었습니다.
실체 클래스는 추상(abstract) 클래스와 상반되는 개념으로 자신의 인스턴스를 생성할 수 있는 일반적인 클래스를 말합니다.
Object 클래스의 final이 아닌 모든 메서드(equals, hashCode, toString, clone, finalize)는 다른 모든 클래스에서 준수해야 하는 보편적 계약(general contracts)을 내포하고 있습니다.
final 메서드는 서브 클래스에서 재정의할 수 없습니다.
모든 객체에 공통적으로 필요한 최소한의 메서드를 Object에 정의하고, 그 메서드들이 미리 정해진 약속에 의해 오버라이드 되어 이용할 수 있도록 한 것을 보편적 계약이라 합니다.
만약 이러한 보편적 계약을 준수하지 않으면, 그 계약에 준하는 다른 클래스와 함께 이용할 때 문제가 발생할 수 있습니다.
우선 보편적 계약 중 equals 메서드에 대해 알아보겠습니다.
인스턴스가 동일한지를 판단해주는 메서드입니다.
우선 equals 메서드를 언제 Override 하는지 알아봅시다.
간단해 보이지만 오버 라이딩하다가 잘못되는 경우가 있기 때문에, 다음 중 어느 하나라도 만족하면 슈퍼 클래스의 equals 메서드를 그대로 이용하는 것이 좋습니다.
클래스의 각 인스턴스가 본래부터 유일한 경우
Thread와 같이 인스턴스가 갖는 값보다는 활동하는 개체임을 나타내는 것이 더 중요한 클래스의 경우, 인스턴스가 갖는 값의 비교는 사실 무의미합니다.
두 인스턴스가 논리적으로 같은지 검사하지 않아도 되는 경우
java.util.Random 클래스는 난수를 발생시키는데, 두 개의 Random 인스턴스가 같은 난수 열을 발생시키는지 확인하기 위해 오버 라이딩할 수 있습니다.
하지만 클래스 설계자는 클라이언트가 이런 기능을 원한다고 생각하지 않았기 때문에 필요 없게 됩니다.
슈퍼 클래스에서 이미 equals 메서드를 오버 라이딩했고, 이 메서드를 그대로 이용해도 좋은 경우
예를 들어, Set 인터페이스를 구현하는 대부분의 클래스는 AbstractSet에 구현된 equals를 상속받아 사용하면 됩니다.
private or package-private 클래스라서 equals 메서드가 절대 호출되지 않아야 하는 경우
따라서 아래와 같이 절대 호출되지 않도록 해야 합니다.
@Override
public boolean equals(Object o) {
throw new AssertionError();
}
장황하게 설명되어 있는데 결론은 상위 클래스의 equals 메서드를 이용할 수 있으면 굳이 오버 라이딩하지 말라는 내용입니다.
객체 참조만으로 동일 여부를 판단하는 게 아니라, 인스턴스가 갖는 값을 비교하여 동일 여부를 판단하는 경우엔 equals 메서드를 오버 라이딩해야 합니다.
일반적으로 하나의 값을 나타내는 Integer나 Date 같은 value 클래스가 여기에 속합니다.
아래 코드를 보면 객체 참조 여부가 아닌, 객체가 갖는 값을 논리적으로 비교합니다.
Enum처럼 각 값 당 최대 하나의 객체만 존재하도록 인스턴스 제어를 이용하는 클래스입니다.
Enum은 값이 일치하는지 비교하는 것이 객체 참조가 일치하는지를 비교하는 것과 동일한 의미이기 때문입니다.
따라서 Object 클래스의 equals 메서드를 이용하면 값 일치와 참조 일치, 두 가지 비교를 동시에 할 수 있습니다.
다시 주제로 돌아와서, equals 메서드를 오버라이드 할 때는 아래와 같은 보편적 계약을 따라야 합니다.
x != null, y != null, z != null을 만족한다는 기본 전제하에 아래와 같습니다.
재귀적(Reflexive) : x.equals(x) = true
대칭적(Symmetric) : x.equals(y) = true 이면 y.equals(x) = true
이행적(Transitive) : x.equals(y) = true, y.equals(z) = true 이면 x.equals(z) = true
일관적(Consistent) : x.equals(y)는 항상 일관성 있게 true or false
x.equals(null) = false
객체들이 단독으로 쓰인다면 문제가 없겠지만, 객체지향 프로그래밍에서는 서로 간의 의존성이 발생하게 됩니다.
만약 위와 같은 보편적 계약을 따르지 않는다면, 의존성이 발생할 수 있는 환경에서 예기치 못한 문제가 발생할 수 도 있습니다.
이 보편적 계약에 대해 좀 더 자세히 알아보겠습니다.
객체를 자기 자신과 비교하면 같아야 합니다.
이를 위반하는 경우는 사실 발생하기 힘든데, 만약 이를 어기고 Collection에 해당 객체를 추가했다면 Collection의 contain 메서드를 호출했을 때 없다고 나올 겁니다.
어떤 두 객체 건 대칭적으로 서로 비교하면 같아야 합니다.
아래 코드를 보면 대칭성을 위반한 경우를 볼 수 있습니다.
대소문자를 구분하지 않는 CaseInsensitiveString 클래스 객체의 equals(String) 메서드를 호출하면 true가 반환됩니다.
하지만 대칭적인 String.equals(CaseInsensitiveString)를 호출하면 반대로 false가 반환됩니다.
이때 list.contains(s)를 호출하면 false가 반환됩니다.
이를 해결하기 위해 아래처럼 String 객체를 처리하는 코드를 제거하면 됩니다.
첫 번째 객체가 두 번째 객체와 동일하고 두 번째 객체가 세 번째 객체와 동일하면, 첫 번째 세 번째 객체가 동일해야 합니다.
아래 코드는 int x 좌표를 갖는 Point 클래스와 이를 상속받아 Color color 값을 가지는 ColorPoint 서브 클래스입니다.
ColorPoint 클래스의 equals 메서드를 위에서처럼 구현하면 대칭성을 위반하게 됩니다.
아래와 같이 equals 메서드를 수정하면 대칭성을 위반하지 않지만 이행성에 문제가 생깁니다.
결론부터 말하면 인스턴스 생성이 가능한 클래스의 서브 클래스에 값 컴포넌트(ex Color)를 추가하면서 equals 계약을 지킬 수 있는 방법이 없습니다.
차선책으로 ColorPoint를 Point의 서브 클래스가 아닌, Point를 필드로 가지는 컴포지션을 이용할 수 있습니다. 이 방법을 이용하면 이행성을 위반하지 않고 equals 메서드를 구현할 수 있습니다.
이 외에도 추상(abstract) 클래스를 이용해도 이행성 문제가 일어나지 않습니다.
만일 두 객체가 동일하다면, 변경되지 않는 한 항상 동일해야 합니다.
이 말은 불변 객체는 항상 동일하거나, 동일하지 않도록 equals 메서드를 작성해야 합니다.
따라서 신뢰할 수 없는 자원에 의존하는 equals 메서드를 작성해서는 안됩니다.
모든 객체는 null과 동일하면 안 됩니다.
우선 equals 메서드에서 비교에 적합한 타입인지 인자를 변환(instanceof)을 이용해서 검사합니다.
이 과정에서 null 체크를 하게 됩니다.
그리고 적합한 인자가 아니라면 ClassCastException 예외가 발생하게 됩니다.
만약 null과 비교하게 된다면 항상 false가 반환됩니다.
1. 객체의 참조만으로 같은 객체인지 비교 가능하다면 == 연산자를 이용하자.
2. instanceof 연산자를 이용해서 전달된 인자의 타입을 확인하자.
3. 인자 타입을 올바른 타입으로 변환하자.
4. 비교해야 하는 필드를 모두 비교하자.
float, double을 제외한 기본형 필드는 == 연산자로 비교하고, 객체 참조일 때는 참조하는 객체의 equals 메서드를 다시 호출합니다.
float는 Float.compare, double은 Double.compare 메서드를 이용합니다.
다른 가능성이 높거나 비교 비용이 적게 드는 필드부터 비교하면 성능 향상에 도움이 됩니다.
5. 대칭적, 이행적, 일관성이 있는지 확인하자.
1. equals 메서드를 오버라이드 할 때, hashCode 메서드도 항상 오버라이드 해야 합니다.
이에 대한 내용은 다음에 기재하겠습니다.
2. 너무 똑똑한 척? 하지 마라.
위의 예시들처럼 단순히 필드 값 비교는 문제가 없지만, 더 복잡한(ex 엘리어싱) 객체들의 비교까지도 고려하고 있는 것은 좋지 않습니다.
3. equals 메서드의 인자 타입을 Object 대신 다른 타입으로 바꾸지 말자.
아래 코드는 인자 타입을 MyClass로 쓰고 있는 오버로드(overload)한 equlas 메서드입니다.
잘 이용하면 편리할 수 있지만, 오히려 코드 복잡도만 늘리게 만들 수 도 있습니다.
public boolean equals(MyClass obj) {
// do something
}
안드로이드 개발 하면서 equals 메서드에 대해 크게 생각 안했는데, 만들 때 생각해야 할 점이 많습니다.