핵심 정리
- 꼭 필요한 경우가 아니라면 equals를 재정의하지 말자.
- 재정의가 필요한 경우에는 아래 설명되어진 다섯 가지의 규약을 지켜가며 비교하자.
다음 중 하나에 해당한다면 재정의하지 않는 것이 최선이다.
- 각 인스턴스가 본질적으로 고유하다.
- 값을 비교할 목적이 아닌 동작하는 개체를 표현하는 클래스가 해당한다. (ex. Thread ..)
- 이러한 클래스들의 경우에는 재정의하지 않고, Object의 `equals`를 사용하여도 무방하다.
- 인스턴스의 '논리적 동치성'을 검사할 일이 없다.
- 여기서 논리적 동치성이란, 두 객체가 의미적으로 같은지를 뜻한다.
- 정규 표현식 클래스인 `Pattern`의 객체는 서로 비교할 일이 없을 경우로, Object의 equals로 해결된다.
- 상위 클래스에서 재정의한 `equals`가 하위 클래스에도 들어맞는다.
- 대부분의 `Set 구현체`는 `AbstractSet`이 구현한 equals를 상속받아 사용하는 것과 같다.
- 클래스의 접근 제어자가 `private`, `package-private`이고, `equals 메서드`를 호출 일이 없다.
- equals가 실수로라도 호출된다면 예외를 터뜨리는 방식으로 구현하자.
그렇다면 재정의해야 할 때는 언제일까?
상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되어 있지 않을 때
- 주로 값 클래스들이 이에 해당한다. (ex. Integer, String ..)
- 하지만 Enum과 같은 '인스턴스 통제 클래스'라면 equals를 재정의하지 않아도 된다.
- 인스턴스 통제 클래스 : 값이 같은 인스턴스가 둘 이상 만들어지지 않음을 보장하는 클래스
- equals를 재정의할 때 따랴아 할 일반 규약
- 반사성 : x.equals(x)는 true이다.
- 대칭성 : x.equals(y)가 true일 경우, y.equals(x)도 true이다.
- 추이성 : x.equals(y)가 true이고, y.equals(z)도 true일 경우, x.equals(z)도 true이다.
- 일관성 : x.equals(y)가 true일 경우, 반복 호출시에도 항상 true이다.
- null-아님 : x.equals(null)은 false이다.
※ 여기서 x, y, z는 null이 아닌 참조 값을 뜻한다.
일반 규약을 어기기 쉬운 상황과 이를 지키기 위한 방법
1. 대칭성
@Override public boolean equals(Object o){
if(o instanceOf CaseInsensitiveString)
return s.equalsIgnoreCase(
((CaseInsensitivesString) o).s);
if(o instanceOf String)
return s.equalsIgnoreCase((String) o);
return false;
}
- 위 코드는 대칭성을 위배했다. 어느 부분에서 위배하였을까?
CaseInsensitiveString cis = new CaseInsensitiveString("Java");
String s = "java";
cis.equals(s); // true
s.equals(cis); // false
- 두 개게를 비교한 결과값이 서로 달라 대칭성이 깨지게 되는데 이는 String 클래스의 equals()는 CaseInsensitiveString을 알지 못하기에 발생하는 문제이다. 그렇다면 이는 어떻게 재정의해야 할까?
@Override public boolean equals(Object o){
return o instanceOf CaseInsensitiveString &&
((CaseInsensitiveString) o).equalsIgnoreCase(s);
}
- 위와 같이 equals를 재정의할 경우, String과의 비교는 배제되어 규약을 지킬 수 있다.
2. 추이성
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;
}
}
class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
}
class Test {
ColorPoint cp = new ColorPoint(1,1,Color.RED);
ColorPoint cp2 = new ColorPoint(1,1,Color.BLUE);
System.out.println(cp.equals(cp2)); // true
}
- 다음과 같이 Point 클래스를 상속받은 ColorPoint 가 있다.
- true가 반환되었지만 색상 정보에 대한 비교는 이뤄지지 않아 이는 '대칭성 위배'이다.
- 그렇다면 자식 클래스에서도 equals를 재정의하면 해결되지 않을까? 다음을 살펴보자
class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Point))
return false;
if (!(o instanceof ColorPoint))
return o.equals(this);
return super.equals(o) && ((ColorPoint) o).color == color;
}
}
class Test {
ColorPoint p1 = new ColorPoint(1, 1, Color.RED);
Point p2 = new Point(1, 1);
ColorPoint p3 = new ColorPoint(1, 1, Color.BLUE);
System.out.println(p1.equals(p2)); // true
System.out.println(p2.equals(p3)); // true
System.out.println(p1.equals(p3)); // false
}
- 이는 추이성에 명백히 위반한다.
- p1과 p2가 true로 반환하고, p2와 p3가 true로 반환하면 p1과 p3 역시 true여야 한다.
- 그렇다면 해법은 무엇일까?
구체 클래스를 확장하여 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
하지만 상속 대신 컴포지션을 사용하여 이를 우회할 수 있는 방법이 존재한다
public class ColorPoint {
private final Point point;
private final Color color;
public ColorPoint(int x, int y, Color color) {
this.point = new Point(x, y);
this.color = Objects.requireNonNull(color);
}
// Point의 equals는 따로 사용 가능
public Point asPoint() {
return point;
}
// 필요 시 ColorPoint끼리의 equals도 별도로 정의
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint)) return false;
ColorPoint cp = (ColorPoint) o;
return point.equals(cp.point) && color.equals(cp.color);
}
}
- 위와 같이 컴포지션을 통해 값이 추가되어도 규약에 벗어나지 않도 재정의가 가능하다.
3. 일관성
- 일관성은 두 객체가 같다면 수정되지 않는 한 영원히 같아야 한다는 의미다
- 즉, 불변 객체는 반드시 일관성을 지켜야 한다.
- 따라서 클래스를 작성할 때 불변 클래스와 가변 클래스 중 심사숙고하는 부분이 필요하다.
- 불변, 가변 여부를 떠나서 equals 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안된다.
- 예를 들어 java.net.URL의 equals는 매핑된 URL과 호스트 IP 주소를 이용해 비교한다.
- 호스트 이름은 IP 주소로 바꾸려면 네트워크를 통해 변경하기에 항상 같다고 보장할 수 없다.
- 그래서 URL과 equals가 일반 규약을 어기게 되어 실무에서도 종종 유사한 문제가 발생하곤 한다.
4. null 아님
// 명시적 검사
@Override
public boolean equals(Object o) {
if (o == null)
return false;
...
}
// 묵시적 검사
@Override
public boolean equals(Object o) {
if (!(o instanceof MyType))
return false;
MyType mt =(MyType) o;
...
}
- null 아님은 모든 객체가 Null과 같지 않아야 한다는 뜻이다.
- 의도하지 않았지만 발생할 경우 `NPE`를 던지게 될 것이다.
- 다음 코드처럼 입력이 null인지를 확인하여 자신을 보호한다.
- null을 해결하지 위해 명시적 방법과 묵시적 방법이 존재하나, 묵시적 방법을 사용하는 것이 바람직하다.
올바르게 equals를 재정의하는 방법을 알아보자.
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(...){...}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber) o;
return pn.lineNum == lineNum
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
}
- == 연산자를 사용하여 입력이 자기 자신의 참조인지 확인한다.
- instanceof 연산자로 입력이 올바른 타입인지 확인한다.
- 입력을 올바른 타입으로 형변환 한다.
- 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 검사한다.
'Java > Effective Java' 카테고리의 다른 글
[이펙티브 자바] Item 11. equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2025.03.29 |
---|---|
[이펙티브 자바] Item 9. try-finally보다 try-with-resource를 사용하라 (0) | 2025.03.24 |
[이펙티브 자바] Item 8. finalizer와 cleaner 사용을 피하라 (0) | 2025.03.24 |
[이펙티브 자바] Item 7. 다 쓴 객체 참조를 해제하라. (0) | 2025.03.24 |
[이펙티브 자바] Item 6. 불필요한 객체 생성을 피하라 (0) | 2025.03.24 |