Search

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

생성일
2023/07/27 10:35
챕터
3장 - 모든 객체의 공통 메서드

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 어노테이션을 붙여 이런 실수를 방지하자.