equals 메서드 재정의하지 않기
•
equals 메서드는 재정의하기 쉬워 보이지만 잘못 작성하면 오류를 발생시키는 코드를 만들기 쉬우므로, 재정의하지 않는게 더 나을 수 있다.
•
아래의 여러 상황들에 해당된다면 재정의하지 않는 것이 더 좋다.
◦
각 인스턴스가 본질적으로 고유한 경우
Thread처럼 값을 표현하는게 아닌 동작하는 개체를 표현하는 클래스인 경우에는, Object의 equals 메서드가 딱 맞게 구현되어 있으므로 재정의하지말고 그대로 사용하면 된다.
◦
인스턴스의 논리적 동치성(logical equality)을 검사할 일이 없는 경우
클라이언트에서 논리적 동치성을 검사할 필요가 없다고 판단하거나, 논리적 동치성을 검사하기를 원치 않는 경우 재정의할 필요없이 Object의 equals 만으로 해결된다.
◦
상위 클래스에서 재정의한 equals가 하위 클래스에도 잘 적용되는 경우
대부분의 Set 구현체들은 AbstractSet이 구현한 equals를 상속받아 사용하고, List 구현체들은 AbstractList에서, Map 구현체들은 AbstractMap에서 상속받아 그대로 사용한다.
◦
클래스가 private거나 package-private이고 equals 메서드를 호출할 일이 없는 경우
위험을 철저하게 피하고 싶어 equals가 실수로라도 불리는 걸 막고 싶다면 아래와 같이 재정의하여 사용을 막자.
@Override
public boolean equals(Object o) {
throw new AssertionError(); // 호출 금지
}
Java
복사
equals 메서드 재정의하기
•
객체 식별성(두 객체가 물리적으로 같은가)을 확인하는 것이 아니라 논리적 동치성을 확인해야 하는 상황에서 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때는 equals 메서드를 재정의 해야한다.
•
equals가 논리적 동치성을 확인하도록 재정의하여, Map의 키로 사용되거나 Set의 원소로 사용할 수 있게 만들어 사용성을 높이자.
•
equals 메서드를 재정의할 때는 아래의 일반 규약을 지켜야 한다.
◦
반사성(reflexivity)
null이 아닌 모든 참조값 x에 대해, x.equals(x)는 true다.
equals 메서드를 통해서는 자기자신은 항상 true가 나와야 한다.
◦
대칭성(symmetry)
null이 아닌 모든 참조값 x, y에 대해, x.equals(y)가 true면 y.equals(x)도 true다.
toString에서 대소문자를 그대로 돌려주지만, equals에서는 대소문자를 무시하도록 구현하는 경우에는 대칭성이 위반될 수 있다.
◦
추이성(transitivity)
null이 아닌 모든 참조값 x, y, z에 대해, x.equals(y)가 true고 y.equals(z)가 true면 x.equals(z)도 true다.
A 클래스를 확장하는 B 클래스가 있고 B 클래스에만 존재하는 필드가 있다면, equals에서 A 클래스의 필드만 검사하면 A와 B를 비교할 때 대칭성을 위배한다. equals에서 B 클래스의 필드까지 검사하면 A 클래스에 B 클래스를 치환할 수 없어 리스코브 치환원칙(LSP)를 위배하게 된다. 그렇다고 아래와 같이 구현하면, A→B→A 순으로 비교할 때 추이성을 위배한다.
@Override
public boolean equals(Object o) {
if (!(o instanceof A)) return false;
if (!(o instanceof B)) return o.equals(this);
return super.equals(o) && ((B) o).color == color;
}
Java
복사
이런 이유로 구체 클래스를 확장하면서 새로운 필드를 추가한 클래스는 완벽히 equals 규약을 만족시킬 수 없다.
Timestamp 클래스를 확장한 Date 클래스는 이런 이유로 equals가 규약을 만족하지 못하고 있다.
◦
일관성(consistency)
null이 아닌 모든 참조값 x, y에 대해, x.equals(y)를 반복해서 호출하면 항상 이전 호출과 같은 값을 반환해야 한다.
두 객체가 같다면 두 객체 모두 수정되지 않는 한 앞으로도 영원히 같아야 한다는 의미이다. 클래스를 작성할 때 불변 클래스로 만드는 것을 고려해야하는 이유 중 하나이다.
가변 클래스더라도 equals에 신뢰할 수 없는 필드로 판단을 해서는 안된다.
◦
Not-Null
null이 아닌 모든 참조값 x에 대해, x.equals(null)은 false다.
의도하지 않았음에도 이런 상황이 발생하는 것은 쉽지 않지만, 실수하게 된다면 NullPointerException이 발생하게 되니 주의하자.
// 명시적 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;
...
}
Java
복사
instanceof는 연산자 앞에 오는 첫 번째 피연산자가 null이면 false를 반환하니 묵시적 검사를 사용하자.
•
좋은 equals를 재정의하는 방법
◦
== 연산자를 사용해 자기 자신의 참조인지 확인하자
단순한 성능 최적화로 비교 작업이 복잡할수록 효율이 좋다.
◦
instanceof 연산자로 입력 타입을 확인하자
null 검사와 함께 올바른 타입으로 비교를 시도하는지 확인하자.
어떤 인터페이스는 자신을 구현한 서로 다른 클래스끼리도 비교할 수 있도록 equals 규약을 수정하기도 하므로, 이런 경우에는 instanceof로 해당 인터페이스의 타입인지 검사하도록 작성하자.
◦
입력을 올바른 타입으로 형변환하자
위에서 istanceof로 검사를 진행했다면 형변환 오류가 발생하지 않으니, 형변환해서 해당 클래스의 접근자를 사용하자.
◦
입력 객체와 자기 자신의 대응되는 핵심 필드들이 전부 일치하는지 검사하자
핵심 필드들을 전부 검사하고 하나라도 틀리면 false를, 그렇지 않다면 true를 반환하자.
float과 double을 제외한 나머지 원시 타입 필드는 == 연산자로 비교하고, 참조 타입 필드는 해당 필드의 equals 메서드로, float과 double은 Float과 Double 박싱 클래스의 compare로 비교하자. Float과 Double의 equals는 오토 박싱을 수반할 수 있으니 성능상 좋지 않다.
null을 받을 수 있는 필드라면 Object.equals(Object1, Object2)로 비교해 NPE 발생을 예방하자.
핵심 필드를 비교하더라도 변경 가능성이 높은 필드들부터 비교하는 것이 성능상 훨씬 도움이 된다.
◦
equals를 다 구현했다면 대칭성, 추이성, 일관성을 만족하는지 확인하자
세 특성을 만족하는지 단위 테스트를 작성해 돌려보자. 만족하지 않는다면 실패 원인을 찾아 수정하자.
◦
equals를 재정의할 땐 hashCode도 반드시 재정의하자
다음 아이템인 아이템 11에서도 말하듯, equals가 논리적 동치를 가지면 hashCode 또한 같은 값을 뱉도록 재정의하자.
◦
너무 복잡하게 해결하려하지 말자
필드들의 동치성 검사만해도 equals의 규약을 지킬 수 있다. 너무 공격적으로 파고들다가 오히려 문제를 발생시킬 수도 있다.
◦
equals의 매개변수는 항상 Object다
boolean equals(Type o) {
...
}
Java
복사
위 코드는 equals를 재정의 한게 아니라 다중정의(overloading)한 것이다. 항상 @Override 어노테이션을 붙여 이런 실수를 방지하자.