꼭 필요한 경우가 아니라면 equals를 재정의하지 말자, 재정의해야할 때는 5가지 규약을 확실히 지키자.
equals를 재정의하지 않아야하는 상황
equals는 재정의할 때 주의해야하는 메서드 중 하나다. 아래와 같은 상황에서는 재정의하지 않는 것이 좋다.
1. 각 인스턴스가 본질적으로 고유할 때
Thread 등 값이 아니라 동작하는 개체를 표현하는 클래스에서 비교할 때, 기본 equals가 아주 적합하므로 재정의할 필요가 없다.
2. 인스턴스의 논리적 동치성을 검사할 일이 없을 때
java.util.regex.Pattern에서 equals 를 재정의해서, 두 정규표현식이 같은지 검사할 수 있다.
하지만 클라이언트가 이 방식을 원하지 않거나 필요없을 때는 굳이 equals를 재정의하지 말자.
3. 상위 클래스에서 재정의한 equals를 하위에서 사용해도 될 때
Set, List, Map은 상위 클래스에서 재정의한 equals를 상속 받아서 그대로 사용하자.
4. private 클래스 일 때 (equals 메서드를 호출할 필요가 없음)
equals를 재정의해야할 때
객체 식별성 (두 객체가 물리적으로 같은지)가 아니라 논리적 동치성 (두 객체의 값이 같은지)를 확인해야하는 상황에서
상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의 되지 않았을 때
주로 값 클래스 Integer, String 등의 클래스에서 재정의해야한다.
재정의한 equals는 Map의 key, Set의 원소 등에서도 사용할 수 있다.
만약 같은 인스턴스가 둘 이상 만들어지지 않음이 보장되는 인스턴스 통제 클래스인 경우에는 equals를 재정의하지 않아도 된다.
왜냐하면 논리적으로 같은 인스턴스가 2개 이상 만들어지지 않기 때문에, 객체 식별성으로 논리적 동치성을 파악할 수 있다.
equals를 재정의할 때 따라야할 일반 규약
한 클래스의 인스턴스는 여러 곳으로 빈번이 전달되고, 컬렉션 클래스를 포함해 많은 클래스는 전달 받은 객체가 equals 규약을 지킨다고 가정하고 동작한다. 따라서 반드시 equals 규약을 지켜서 재정의해야한다.
1. 반사성 (reflexivity)
- null 이 아닌 모든 참조값 x 에대해 x.equals(x)는 true다.
- 즉, 객체는 자기 자신과 같아야한다.
2. 대칭성 (symmetry)
- null 이 아닌 모든 참조값 x,y 에 대해 x.equals(y)가 true면 y.equals(x) 도 true다.
- 즉, 두 객체는 서로에 대한 동치 여부에 똑같은 결과를 내야한다.
ex) 대칭성을 위반하는 예시
대소문자를 구별하지 않는 문자열 CaseInsensitiveString 에서 equals 재정의 할때
CaseInsensitiveString 객체에서 equals를 사용하면 true를 반환하지만, String 객체에서 equals를 사용하면 false를
반환하여 대칭성을 명백히 위반한다.
public final class CaseInsensitiveString {
private final String s;
public CaseInsensitiveString(String s){
this.s = Objects.requireNonNull(s);
}
@Override public boolean equals(Object o){
if (o instance of CaseInsensitiveString){
return s.equalsIsgnoreCase(
((CaseInsensitiveString) o).s);
}
if (o instance of String){ // 한 방향으로만 작동
return s.equalsIgnoreCase((String) o);
}
return false;
}
}
// 사용 예시
CaseInsensitiveString cis = new CaseInsensitiveString("HelloWorld")
String s = "helloworld";
system.out.println(cis.equals(s)) // true
system.out.println(s.equals(cis)) // false
따라서 아래처럼 String의 equals와 같은 결과가 나오도록 수정해야한다.
@Override public boolean equals(Object o){
return o instanceof CaseInsensitiveString &&
((CaseInsensitiveString o).s.equalsIgnoreCase(s);
}
3. 추이성(transitivity)
- null 이 아닌 모든 참조값 x, y, z에 대해 x.equals(y)가 true이고 y.equals(z)도 true면 x.equals(z)도 true 다.
- 즉, x 와 y 가 같고, y 와 z가 같을 때 x와 z는 같아야한다.
ex) 추이성을 위반하는 예시
하위 클래스에 새로운 필드 (색상)를 추가하는 상황
public class Point {
private final int x;
private final int y;
public Point(int x, int y){
this.x = x;
this.y = y;
}
@Override public boolean equals(Object o){
if(!(o instanceof Point)){
return false;
}
Point p = (Point)o;
return p.x == x && p.y == y;
}
}
// 하위 클래스
public class ColorPoint extends Point{
private final Color color;
public ColorPoint(int x, int y, Color color){
super(x, y);
this.color = color;
}
}
이때 equals를 그대로 두는 경우, equals 규약을 어기는 것은 아니지만
color라는 필드를 무시하므로 논리적으로 좋지 않다.
ColorPoint에서 위치와 색상이 같을 때만 true를 반환하는 equals를 재정의한다면?
Point 객체에서 equals 결과와 ColorPoint에서의 equals 결과가 달라 대칭성을 위반한다.
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
System.out.println(p.equals(cp)); // true
System.out.println(cp.equals(p)); // false
ColorPoint와 Point를 비교할 때는 색상을 무시하는 equals로 재정의한다면?
대칭성은 지키지만 추이성을 위반한다. (x==y, y==z 이지만 x!=z임)
@Override public boolean equals(Object o){
if(!(o instanceof Point)){
return false;
}
// o가 일반 Point면 색상을 무시하고 비교
if(!(o instanceof ColorPoint)){
return o.equals(this);
}
// o가 ColorPoint이면 색상까지 비교
return super.equals(o) && ((ColorPoint) o).color == color;
}
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
System.out.println(p1.equals(p2)); // true
System.out.println(p2.equals(p3)); // true
System.out.println(p3.equals(p1)); // false
⭐️ 하위 클래스에 새로운 값을 추가하면서 equals 규약을 만족시킬 수 있는 법은 존재하지 않는다.
리스코프 치환 원칙에 따르면 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 중요하다.
즉, 하위 클래스에서도 여전히 같은 클래스이므로 같은 방식으로 활용될 수 있어야한다는 뜻이다.
equals에서 발생하는 상속 문제를 피하기 위해, 상속 대신 컴포지션을 사용하는 방식으로 우회할 수 있다.
⭐️ ColorPoint는 Point를 상속하지 않고 내부에 포함(컴포지션)하므로, Point와 ColorPoint는 서로 다른 타입이 되어 비교 대상이 되지 않는다.
이때, ColorPoint를 Point처럼 사용할 수 있도록 같은 위치의 Point 인스턴스를 반환하는 view 메서드 (asPoint())를 제공하면 된다.
// Point를 상속받는게 아니라 컴포지션
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color){
point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
// Point 뷰 반환
public Point asPoint(){
return point;
}
@Override public boolean equals(Object o){
if(!o instanceof ColorPoint)){
return false;
}
ColorPoint cp = (ColorPoint) o;
return cp.point.equals(point) && cp.color.equals(color);
}
}
4. 일관성 (consistency)
- null 이 아닌 모든 참조값 x, y에 대해 x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나, false를 반환한다.
- 즉, 두 객체가 같다면 객체가 수정되지 않는 한 앞으로도 영원히 같아야한다.
클래스의 불변, 가변과 상관 없이 equals에 신뢰할 수 없는 자원을 넣으면 안된다.
ex) java.net.URL의 equals
equals는 주어진 URL 뿐아니라, 내부적으로 DNS조회를 해서 같은지 확인한다.
이는 네트워크 상황에 따라 달라질 수 있으므로, 이 메서드를 실무에서 사용하면 안된다.
따라서 equals는 메모리에 존재하는 객체만 사용해서 결정적 계산을 할 때만 사용해야한다.
5. null-아님
- null 이 아닌 모든 참조 값 x에 대해 x.equals(null)은 false다.
- 즉, 모든 객체가 null과 같지 않아야한다.
명시적으로 null이면 false를 리턴하는 equals를 정의할 필요가 없다.
동치성을 검사하기 위해서는, 입력 매개변수가 같은 타입인지 확인하는 묵시적 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;
}
올바른 equals를 재정의하는 순서
1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
- 자기 자신이면 true 반환
- 성능 최적화용, 비교 작업이 복잡할 때 유용
2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
- 올바른 타입이 아니라면 false 반환
- 인터페이스를 구현한 컬렉션 (Set, List, Map..) 등은 equals에서 인터페이스를 사용해야한다.
3. 입력을 올바른 타입으로 형변환 한다.
4. 입력 객체와 자신의 대응되는 핵심 필드가 모두 일치하는지 하나씩 검사한다.
- 모두 일치할 때만 true, 하나라도 다르면 false 반환
- float, double 제외 기본 타입 필드 ➡️ == 연산자로 비교
- 참조타입 필드 ➡️ equals로 비교
- float, double ➡️ Float.compare(float, float)과 Double.compare(double, double)로 비교
- Null ➡️ Object.equals(Object, Object)로 비교
만약 비교하기 복잡한 필드를 가진 클래스의 경우, 필드의 표준형을 저장하고 표준형끼리 비교하는 것이 좋다.
어떤 필드를 비교하는지에 따라 equals 성능이 달라지므로, 비교 비용이 싼 필드를 먼저 비교하자
5. 구현한 equals가 대칭성, 추이성, 일관성을 만족하는지 단위테스트를 한다.
- 구글에서 만든 AutoValue 프레임워크를 사용하면, 테스트 작성을 쉽게 할 수 있다.
'도서 > 이펙티브 자바' 카테고리의 다른 글
| [이펙티브 자바] 12. toString을 항상 재정의하라 (4) | 2025.07.24 |
|---|---|
| [이펙티브 자바] 11. equals를 재정의하려거든 hashCode도 재정의하라 (2) | 2025.07.21 |
| [이펙티브 자바] 9. try-finally 보다는 try-with-resources를 사용하라 (0) | 2025.04.15 |
| [이펙티브 자바] 8. finalizer 와 cleaner 사용을 피하라 (0) | 2025.03.23 |
| [이펙티브 자바] 7. 다 쓴 객체 참조를 해제하라 (0) | 2025.03.21 |