equals 메서드는 재정의하기 쉬워 보이지만 곳곳에 함정이 도사리고 있다. 따라서 재정의가 필요하지 않은 경우에는 재정의하지 않는 것이 최선의 선택이다.
값을 표현하는 게 아니라 동작하는 개체를 표현하는 클래스
Thread
와 같은 클래스가 좋은 예시다.
❗️ 참고
equals가 실수로라도 호출되는 걸 막고 싶다면 다음처럼 구현하자.
@Override public boolean equals(Object o) { throw new AssertionError(); // 호출 금지! }
그것은 바로 객체 식별성
(Object identity)이 아니라 논리적 동치성
(logical equality)을 확인해야 하는데, 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때이다.
주로 값 클래스
들이 여기에 해당한다.
값 클래스 중에서도, 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 인스턴스 통제 클래스나 Enum 타입은 재정의할 필요가 없다. 이런 경우는 객체 식별성이 논리적 동치성과 사실상 똑같기 때문이다.
그렇다면 이제 equals 메서드를 재정의할 때 반드시 따라야 하는 일반 규약을 알아보자.
다음은 Object 명세에 적힌 규약으로, 동치관계를 만족하기 위한 요건이다.
💡 동치관계란?
집합을 서로 같은 원소들로 이뤄진 부분집합으로 나누는 연산을 말한다. 이 부분집합을 동치류(equivalence class, 동치 클래스) 라고 한다.
null 이 아닌 모든 참조 값 x 에 대해, x.equals(x)는 true 다.
객체는 자기 자신과 같아야 한다는 뜻이다. 이 요건은 만족하지 못하기가 더 어려워 보인다.
null 이 아닌 모든 참조 값 x, y 에 대해, x.equals(y)는 true 면 y.equals(x)도 true 다.
두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다는 뜻이다. 이 요건은 자칫하면 어길 수 있어 보인다.
null 이 아닌 모든 참조 값 x, y, z 에 대해, x.equals(y)는 true 이고 y.equals(z)도 true 면 x.equals(z)도 true 다.
첫 번째 객체와 두 번째 객체가 같고, 두 번째 객체와 세 번째 객체가 같다면, 첫 번째 객체와 세 번째 객체도 같아야 한다는 뜻이다. 이 요건도 자칫하면 어기기 쉽다.
❗️ 주의
구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
그러나 상속 대신 컴포지션을 사용하는 방식으로 구현할 수는 있다.다만, 추상 클래스의 하위 클래스에서는 equals 규약을 지키면서도 값을 추가할 수 있다.
null 이 아닌 모든 참조 값 x, y 에 대해, x.equals(y)를 반복해서 호출하면 항상 true 를 반환하거나 항상 false 를 반환한다.
두 객체가 같다면 어느 하나가 수정되지 않는 한, 앞으로도 영원히 같아야 한다는 뜻이다. 가변 객체는 비교 시점에 따라 서로 다를 수도 혹은 같을 수도 있지만, 불변 객체는 한번 다르면 끝까지 달라야 한다.
클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안 된다. 이 제약을 어기면 일관성 조건을 만족시키기가 매우 어렵다. equals는 항시 메모리에 존재하는 객체만을 사용한 결정적(deterministic) 계산만 수행해야 한다.
null 이 아닌 모든 참조 값 x 에 대해, x.equals(null) 은 false 다.
마지막 요건은 공식 이름은 없고, 저자가 임의로 'null-아님'이라고 부르고 있다. 이것은 모든 객체가 null과 같지 않아야 한다는 뜻이다.
이를 위해서는 입력이 null인지를 검사해야 하는데, 명시적 null 검사보다는 다음과 같은 묵시적 null 검사가 더 낫다.
// 명시적 null 검사
@Override
public boolean equals(Object o) {
if(o == null)
return false;
...
}
// 묵시적 null 검사
@Override
public boolean equals(Object o) {
if(!(o instanceof MyType))
return false;
MyType mt = (MyType) o;
...
}
묵시적 null 검사의 경우, 첫 번째 피연산자가 null 이면 false를 반환하므로 명시적으로 null 검사를 하지 않아도 된다.
그럼 이제 양질의 equals 메서드 구현 방법을 단계별로 알아보자.
==
연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
이는 단순한 성능 최적화용이다. 비교 작업이 복잡한 상황일 때 값어치를 할 것이다.
instanceof
연산자로 입력이 올바른 타입인지 확인한다.
입력을 올바른 타입으로 형변환
한다.
이 단계는 2번에서 검사를 했기 때문에 100% 성공한다.
입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
모든 필드가 일치하면 true, 하나라도 다르면 false를 반환한다.
equals를 재정의할 땐 hashCode
도 반드시 재정의하자. (아이템 11)
너무 복잡하게 해결하려 들지 말자. 필드들의 동치성만 검사해도 equals 규약을 어렵지 않게 지킬 수 있다.
Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자. 이는 재정의가 아니라 다중 정의한 것이다.
💡 참고
equals를 작성하고 테스트하는 일은 지루하고 이를 테스트하는 코드도 뻔하다.
이 작업을 대신해줄 오픈소스가 있는데, 그것은 바로 구글이 만든AutoValue
프레임워크다.
클래스에@AutoValue
어노테이션만 추가하면 메서드들을 알아서 작성해준다.
https://github.com/google/auto/blob/master/value/userguide/index.md
📌 핵심 정리
꼭 필요한 경우가 아니면 equals를 재정의하지 말자.
많은 경우에 Object의 equals가 원하는 비교를 정확히 수행해준다.
재정의해야 할 때는 그 클래스의 핵심 필드 모두를 빠짐없이, 다섯 가지 규약을 확실히 지켜가며 비교해야 한다.