스터디/이펙티브 자바

[Effective Java] Item10. equals는 일반 규약을 지켜 재정의

📝 작성 : 2022.05.21  ⏱ 수정 : 
728x90

equals를 재정의하면 안되는 경우

  1. 각 인스턴스가 본질적으로 고유한 경우: 값을 표현하는 것이 아닌 동작하는 개체를 표현하는 경우
  2. 인스턴스의 '논리적 동치성'을 검사할 일이 없는 경우
  3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 적용 가능한 경우
  4. private 또는 packaage-private(default) 클래스이면서 equals메서드를 호출하지 않는 경우

그럼 언제 equals를 재정의할까요?

객체 식별성이 아니라 논리적 동치성을 확인해야 하는데, 상위 클래스의 equals로는 불가능한 경우. 주로 값 클래스들이 여기에 해당합니다.

equals를 재정의할 때 지켜야 하는 규약

equals 메서드는 null이 아닌 개체 참조에 대해 등가 관계를 구현
null이 아닌 모든 참조값 x,y,z에 대하여

반사성(reflexity): x.equals(x) == true
대칭성(symmetry): x.equals(y) == true => y.equals(x) == true
추이성(transitivity): x.equals(y) == true && y.equals(z) == true => x.equals(z) == true
일관성(consistency): x.equals(y)를 언제 호출해도 항상 같은 결과를 반환
x.eqauls(null) == false

반사성(reflexity)

객체는 자기 자신과 같아야 합니다. 이를 성립하지 않는 경우 컬렉션에서 contains메서드를 호출해도 찾을 수 없을 것 입니다.

대칭성(symmetry)

두 객체는 서로에 대한 동치여부가 똑같아야 합니다. (x.equals(y) == true => y.equals(x) == true)

public class CaseInsensitiveString {
    private final String s;

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

    @Override
    public boolean equals(Object o) {
        if (o instanceof CaseInsensitiveString) {
            return s.equalsIgnoreCase(((CaseInsensitiveString) o).s);
        }

        if (o instanceof String) { //얘 때문에 잘못됨
            return s.equalsIgnoreCase((String) o);
        }

        return false;
    }
}

위의 경우

CaseInsensitiveString cis = new CaseInsensitiveString("TEST");
System.out.println(cis.equals("test")); //true
System.out.println("test".equals(cis)); //false

CaseInsensitiveString는 String 클래스인 경우도 생각하지만(if (o instanceof String)) String클래스는 CaseInsensitiveString클래스는 모르기 때문에 대칭성이 성립하지 않습니다.

대칭성이 성립하지 않는 경우 컬렉션의 contains메서드를 호출했을 때 JDK 구현에 따라 true/false/런타임 예외를 반환합니다.

추이성(transitivity)

쉽게 삼단논법을 생각하면 됩니다. (A=B이고 B=C이면 A=C)
하위 클래스에서 상위 클래스에는 없는 새로운 필드를 추가하는 경우 어기기 쉽습니다.

public 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 o) {
        if (!(o instanceof Point2D)) return false;
        Point2D p = (Point2D) o;
        return p.x == x && p.y == y;
    }
}

public class Point3D extends Point2D {
    private final int z;

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

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point3D)) return false;
        return super.equals(o) && ((Point3D) o).z == z; //잘못됨
    }
}

이 경우

Point2D point2D = new Point2D(1, 2);
Point3D point3D = new Point3D(1, 2, 3);

System.out.println(point2D.equals(point3D)); //true
System.out.println(point3D.equals(point2D)); //false

이럴 떄는 상속 대신 컴포지션을 사용합니다.

public class Point3D {
    private final Point2D point;
    private final int z;

    public Point3D(int x, int y, int z) {
        point = new Point2D(x, y);
        this.z = z;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point3D)) return false;
        Point3D p = (Point3D) o;
        return p.point.equals(point) && p.z == z;
    }
}

일관성(consistency)

equals의 판단에 신뢰할 수 없는 자원이 있어서는 안됩니다.

URL url1 = new URL("https://sinau.tistory.com");
URL url2 = new URL("https://sinau.tistory.com");
System.out.println(url1.equals(url2));

이 경우 주어진 URL과 매핑된 호스트의 IP주소를 이용해 비교하는데 이 때문에 항상 같은 결과가 나온다는 보장이 없습니다.

null-아님

o.equals(null)은 항상 false를 반환해야 하며 NullPointerException도 허용하지 않습니다. 그렇다고 항상 null검사를 할 필요는 없습니다.
instanceof는 첫 번째 피연산자가 null이면 false를 반환하기 떄문에 명시적으로 null검사를 할 필요가 없습니다.

좋은 equals 구현방법

  1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인, 성능 향상용
  2. instanceof 연산자로 입력이 올바른 타입인지 확인
  3. 올바른 타입으로 형변환
  4. 핵심 필드들이 모두 일치하는지 확인
  5. float와 double을 제외한 기본타입필드는 ==으로 비교
  6. float는 Float.compare(float, float), double은 Double.compare(double, double)로 비교
    • Float.NaN, -0.0f 와 같은 특수한 부동소수 값을 다뤄야하기 때문
    • Float.equals, Double.equals는 오토박싱을 수반할 수 있으므로 성능상 좋지 않습니다.
  7. 배열의 모든 원소가 핵심필드라면 Arrays.equals를 사용
  8. null도 정상 값으로 취급하는 경우 Objects.equals(Object, Object)를 이용해 NPE 발생을 예방
  9. 어떤 필드를 먼저 비교하느냐가 성능에 영향을 끼치므로 비교순서를 잘 생각하여 구성
  10. eqauls를 재정의할때는 반드시 hashCode도 재정의!
반응형