본문 바로가기
개인 공부/Java (이펙티브 자바)

[이펙티브 자바] 아이템 10 : equals는 일반 규약을 지켜 재정의하라

by 희조당 2023. 1. 31.
728x90

🎯 학습 목표

  • Equals의 재정의
  • 고려할 규약들

📌 Equals의 재정의

equals를 오버라이딩하는 것은 NPE를 던지거나 원하는 대로 작동하지 않을 가능성이 높다.

재정의를하지 않고 그대로 equals를 사용하게 된다면 인스턴스는 자기 자신만 같다고 표현한다.

 

✍️ 동일성 vs 동등성 : 동일성은 같은 참조인지 동등성은 같은 값을 가지는지 판단

🤔 언제 하지 말아야 할까?

1️⃣ 각 인스턴스가 본질적으로 고유

값이 아닌 동작을 나타내는 인스턴스는 같은 인스턴스가 애초에 없다. (Ex. Thread) Object의 equals로 충분하다.

2️⃣ 인스턴스의 논리적 동치성을 검사할 일이 없다.

논리적 동치성을 검사할 일이 없다는 것은 단순 동일성을 검사한다는 의미이다. Object의 equals로 충분하다.

3️⃣ 이미 상위 클래스에서 재정의한 Equals가 존재

상위 클래스에 이미 정의된 equals가 존재한다면 굳이 재정의할 필요가 없다.

4️⃣ 클래스가 private이거나 package-private이면 Equals를 호출할 일이 없다.

혹시라도 equals의 호출을 막고 싶으면 다음과 같이 구현하면 된다.

@Override
public boolean equals(Object o) {
  throw new AssertionError(); // 호출 금지!
}

🧐 이럴 때 재정의하기

객체 식별성(동일성)이 아니라 논리적 동치성(동등성)을 비교하는 경우에는 equals를 오버라이딩해야 한다.

또한, 상위 클래스에서 재정의되지 않은 경우도 오버라이딩을 해야 한다.

 

단, 인스턴스가 단 하나만 만들어지는 경우는 비교할 일이 없기 때문에 equals를 재정의할 필요 없다.


📌 고려할 규약들

1️⃣ 반사성

객체는 자기 자신과 같아야 한다. 이 조건을 만족하지 않기가 더 힘들다.

2️⃣ 대칭성

두 객체의 결과는 같아야 한다. 객체 x, y에 대해서 x.equals(y)가 참이면 y.equals(x)가 참이어야 한다.

책에서 제공된 예시는 다음과 같다.

public final class CaseInsensitiveString {
    private final String str;

    public CaseInsensitiveString(String str) {
        this.str = Objects.requireNonNull(str);
    }

    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString){
            return str.equalsIgnoreCase(((CaseInsensitiveString)o).str);
        }
        if (o instanceof String) { // 대칭성 위배
            return str.equalsIgnoreCase((String) o);
        }
        return false;
    }
}

오버라이딩된 equals를 보면 String를 고려해서 작성하지만 이는 명백히 대칭성 위반이다.

 

CaseInsensitiveStringString을 알고 있지만 반대로 String은 모른다. 따라서 다음과 같은 상황이 발생한다.

CaseInsensitiveString cis = new CaseInsensitiveString("hejow");
String s = "hejow";

cis.equals(s); // true
s.equals(cis); // false

 

대칭성을 위반했기 때문에 나아가 다음과 같은 코드에서도 문제가 발생한다.

List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);

list.contains(s); // true or false

JDK에 따라서 contains()가 다른 결과를 반환할 수 있다.

이렇게 equals 규약을 어기면 그 객체를 사용할 때 다른 객체가 어떻게 반응할지 알 수 없다.

 

따라서 다음과 같이 대칭성을 만족시켜서 구현해야 한다.

@Override
public boolean equals(Object obj) {
    return obj instanceof CaseInsensitiveString &&
            ((CaseInsensitiveString) obj).str.equalsIgnoreCase(str);
}

3️⃣ 추이성

추이성은 일종의 삼단논법이다.  인스턴스 x, y, z에 대해서 x와 y가 같고, y와 z가 같다면 x와 z도 같아야 한다.

책에서 제공된 예제를 보고 이해해 보자.

public class Point {
    private final long x ;
    private final long y;

    public Point(long x, long y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }
}
public class ColorPoint extends Point{
    private final Color color;

    public ColorPoint(long x, long y, Color color) {
        super(x, y);
        this.color = color;
    }
}

 

Point에 정의된 equals를 사용하면 대칭성과 반사성은 성립된다. 하지만 추가된 색상까지는 비교할 수 없다.

Point point = new Point(0,1);
ColorPoint bluePoint = new ColorPoint(0, 1, Color.BLUE);

point.equals(bluePoint); //true
bluePoint.equals(point); //true

 

색상까지 비교하기 위해서 ColorPointequals를 다음과 같이 재정의한다.

@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}

public static void main(String[] args) {
    Point point = new Point(0,1);
    ColorPoint bluePoint = new ColorPoint(0, 1, Color.BLUE);
    ColorPoint redPoint = new ColorPoint(0, 1, Color.RED);

    bluePoint.equals(point); // true
    point.equals(redPoint); // true
    bluePoint.equals(redPoint); // false
}

비교해 보면 대칭성은 만족하나 추이성이 깨지는 것을 볼 수 있다.

이와 같이 자식 클래스에서 추가된 필드를 포함하면서 equals 규약을 지킬 수 없다.

🤔 하위 클래스의 필드도 비교할 수 없을까?

상속 대신 컴포지션을 사용한다면 하위 클래스의 필드도 비교할 수 있다.

public class ColorPointFromComposition {
    private final Point point;
    private final Color color;

    public ColorPointFromComposition(int x, int y, Color color) {
        point = new Point(x, y);
        this.color = Objects.requireNonNull(color);
    }

    // (아이템6) Point 뷰를 반환한다.
    public Point asPoint() {
        return point;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPointFromComposition)) {
            return false;
        }
        ColorPointFromComposition cp = (ColorPointFromComposition) o;
        return cp.point.equals(point) && cp.color.equals(color);
    }
}

4️⃣ 일관성

두 객체가 같다면 영원히 같아야 한다. 시간이 흐르거나 호출 횟수가 많아져도 결과가 달라지면 안 된다는 뜻이다.

특히, 불변 클래스는 항상 동일해야 한다. 그리고, equals()의 반환 결과에 신뢰할 수 없는 자원이 들어가서는 안된다.

예시로, java.net.URLequals는 주어진 URL과 매핑된 호스트의 IP 주소를 통해 비교한다.

호스트 이름을 IP 주소로 바꾸려면 네트워크를 통해야 하는데 결과가 항상 같음을 보장할 수 없다.

5️⃣ Not-null

모든 객체는 null과 같지 않아야 한다. 즉, 모든 객체는 객체.equals(null)은 거짓이어야 한다.

앞서 말했듯이 equals()를 잘못 사용하면 NPE이 발생한다. 따라서 NPE에 대해서도 안전해야 한다.

그렇다고 객체와 null을 비교하지 말고 instanceof를 사용하면 된다.

// 직접 확인하기 보단
@Override
public boolean equals(Object o) {
	if (o == null) return false;
}

// instanceof로 확인하자
@Override 
public boolean equals(Object o) {
	if (!(o instanceof Point)) return false;
}

😎 정리

equals를 재정의할 때 발생할 수 있는 이슈들이 있다.

해당 이슈들을 피하기 위해서는 굳이 필요 없는 상황을 피해야 하고, 제시된 규약을 잘 따라야 한다.

재정의해야 할 상황이라면 다음 Step들을 지키자!

  • == 연산자로 반사성을 확인
  • instanceof 연산자로 입력이 올바른 타입인지 확인
  • 입력을 올바른 타입으로 형변환
  • 입력 객체와 자기 자신의 대응되는 핵심필드들이 모드 일치하는지 하나씩 검사
  • equals를 재정의할 때는 hashCode도 반드시 재정의

 

😋 지극히 개인적인 블로그지만 훈수와 조언은 제 성장에 도움이 됩니다 😋

댓글