본문 바로가기
우아한테크코스/공부

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

by 자바지기 2022. 3. 2.
반응형

equals를 재정의할 때 곳곳에 함정이 존재하므로 주의해야 한다.

다음과 같은 상황들에서는 equals를 재정의하지 않아야 한다.

equals를 재정의하지 않는 것은 인스턴스는 오직 자기 자신과만 같다는 것이다.

재정의 하지 말아야 할 때

1. 각 인스턴스가 본질적으로 고유할 때

값을 표현하는 게 아니라 동작하는 개체를 표현하는 클래스에서는 재정의를 하지 않는다.

예를 들어 controller, view 등등
이미 Object에 equals 메서드가 잘 구현되어있다.

// Object의 equals 메소드
public boolean equals(Object obj) {
    return (this == obj);
}

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

3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.

예 :

AbstractSet이 구현한 equals를 Set 구현체에서 상속받아서 사용

// 추상 클래스 AbstractSet의 equals 메서드
// 이 메서드를 Set 구현체에서도 사용한다.
public boolean equals(Object o) {
    if (o == this)
        return true;

    if (!(o instanceof Set))
        return false;
    Collection<?> c = (Collection<?>) o;
    if (c.size() != size())
        return false;
    try {
        return containsAll(c);
    } catch (ClassCastException | NullPointerException unused) {
        return false;
    }
}

4. 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.

5. 인스턴스 통제 클래스 ( Enum )

인스턴스 통제 클래스라면 어차피 논리적으로 같은 인스턴스가 2개 이상 만들어지지 않으므로 논리적 동치성을 확인할 필요가 없다.

재정의 해야할 때

1. 값이 같은지 알고싶을 때

객체가 같은지가 아니라 값이 같은지 알고 싶을 때 equals를 재정의해야 한다.

equals가 논리적 동치성을 확인하도록 재정의해두면, Map의 키와 Set의 원소로 사용할 수 있게 된다.

equals 메서드 재정의 시 일반 규약

- 반사성 : x.equals(x) = true ( x != null )

- 대칭성 : x.equals(y) = y.equals(x) ( x != null, y != null )

- 추이성 : x.equals(y) = true, y.equals(z) = true -> x.equals(z) = true

- 일관성 : x.equals(y) 는 항상 일정

- null-아님 : x.equals(null) = false

null을 검사하기 위해서 instanceof를 사용하면 된다!

ex)

public boolean equals(Object o) { 
    if (!(o instanceof MyType)) {
        return false;
    }

    ...

 }

위의 equals 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할 지 알 수 없다.

예를 들어

x.equals(y) = true
y.equals(x) = false

인 x, y가 존재한다고 할 때

List list = new ArrayList();
list.add(x);

// 아래의 결과는 true 일까? false일까?
list.contains(y)

equals 메서드 일반 규약을 어긴 예

class Point2D {
    private final int x;
    private final int y;

    public Point2D(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point2D)) {
            return false;
        }

        Point2D p = (Point2D) obj;
        return p.x == x && p.y == y;
    }
}


// Point2D를 상속받은 Point3D
class Point3D extends Point2D {

    private final int z;

    public Point3D(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

// Point3D는 필드변수 z까지 같아야함!
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Point3D)) {
            return false;
        }
        return super.equals(obj) && ((Point3D) obj).z == z;
    }
}

이 두 클래스에 대해서 equals를 실행하면 다음과 같다.

public class main {
    public static void main(String[] args) {
        Point2D p2 = new Point2D(1, 2);
        Point3D p3 = new Point3D(1, 2, 3);
        System.out.println(p2.equals(p3)); //true 
        System.out.println(p3.equals(p2)); //false
    }
}

위를 통해 대칭성을 위배한 경우를 확인할 수 있다.

객체 지향적 추상화의 이점 (추상 클래스 상속)을 포기하지 않는 한

이런식으로 구체 클래스를 확장해 새로운 값을 추가하면서 eqauls 규약을 만족시킬 방법은 존재하지 않는다.

추상 클래스의 하위 클래스에서 새롭게 필드 변수가 추가되므로 새롭게 생긴 필드 변수에 대해서는 equals를 적용할 수 없게 되므로..

이 문제를 우회적으로 해결하기 위해서는 상속 대신 컴포지션을 활용하면 된다. (아이템 18)

equals 메서드 구현 방법

이상적인 equals 메서드의 예

class Point2D {
    private final int x;
    private final int y;

    public Point2D(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj) {
        // 1
        if (obj == this) {
            return true;
        }

        // 2
        if (!(obj instanceof Point2D)) {
            return false;
        }

        //3
        Point2D p = (Point2D) obj;

        //4
        return p.x == x && p.y == y;
    }
}

1. == 연산자를 통해 자기 자신의 참조인지 확인한다.

단순 성능 최적화용

2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.

입력이 올바르지 않다면 false를 반환한다.

3. 입력을 올바른 타입으로 형변환한다.

2번에서 instanceof 검사를 했으므로 이 변환은 100% 성공

4. 핵심 필드들이 모두 일치하는지 검사한다.

이를 통해 최종적으로 true인지 false인지 확인한다.

필드 들을 비교할 때는 다를 가능성이 크거나 비교하는 비용이 싼 순서대로 비교한다.

5. equals를 다 구현하고, 대칭적인가? 추이성이 있는가? 일관적인가? 확인한다.

6. equals를 재정의할 땐 hashCode도 반드시 재정의하자

추가 사항

1. float와 double은 특수한 부동소수 값을 다뤄야 하므로

Float.compare(float, float) 
Double.compare(double, double)

로 비교한다.

2. 입력 타입은 반드시 Object여야 한다.

잘못된 예 :

@Override
public boolean equals(Point2D p2) {

위의 메서드는 Object.equals를 재정의한 것이 아니라 컴파일 에러가 발생한다.

이 메서드는 eqauls를 다중 정의한 것이다. 타입을 구체적으로 명시하지말자.

핵심

꼭 필요한 경우가 아니면 equals를 재정의하지말자.

재정의할거면 핵심 필드를 모두 빠짐없이 규약을 지켜가며 비교하자.

반응형

댓글