2 minute read

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

  • equals() 메서드는 재정의가 쉬우나, 잘못 작성하게 되면 의도하지 않는 결과들이 초래되기 쉬우므로 Effective Java에서는 변경하지 않는 것을 권장한다.
    • 많은 경우에 Object.equals가 원하는 비교를 정확히 수행해 줄 것이다.

재정의하지 않아도 되는 경우

  • 각 인스턴스가 본질적으로 고유한 경우
    • 값이 아닌 동작을 표현하는 클래스이다. -> Thread
  • 인스턴스의 논리적인 동치성을 검사할 일이 없는 경우
  • 상위 클래스에서 재정의한 equals가 하위 클래스에서도 동일하게 적용되는 경우
  • 클래스가 private이거나, package-private여서 클라이언트 코드에서 equals를 호출할 일이 없다.
  • 싱글턴임을 보장하는 클래스일때 : 객체간 동등성 및 동일성이 보장된다.

재정의시 규약

반사성 (reflexivity)

  • null이 아닌 모든 참조 값 x에 대해 x.equals(x)를 만족한다.

대칭성 (symmetry)

  • null이 아닌 모든 참조 값 x,y에 대해, x.equals(y) == true이면 y.equals(x) == true이다.

추이성

  • null이 아닌 모든 참조 값 x, y, z에 대해 x.equals(y)가 true이고, y.equals(z)가 true이면 x.equals(z)도 true가 되야 한다.
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 this.x == p.x && this.y == p.y;  
  }  
  ...
}
class ColorPoint extends Point {  
    
  private final Color color;  
  
  @Override  
  public boolean equals(Object o) {  
    if(!(o instanceof ColorPoint)) return false;  
      
    return super.equals(o) && this.color == ((ColorPoint) o).color;  
  }  
}

void test() {
    ColorPoint a = new ColorPoint(2, 3, Color.RED);
    Point b = new Point(2, 3);
    ColorPoint c = new ColorPoint(2, 3, Color.BLUE);

    System.out.println(a.equals(b)); // true
    System.out.println(b.equals(c)); // true
    System.out.println(a.equals(c)); // false
}
  • a.equals(b)는 true를 만족하고 b.equals(c)는 true를 만족하지만 a.equals(c)는 false가 된다.
  • 위의 코드는 equals 정의 규약 중 추이성을 위반하는 코드가 된다.

무한 재귀호출

public class SmellPoint extends Point {

    private final Smell smell;

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

        // o가 일반 Point이면 색상을 무시햐고 x,y 정보만 비교한다.
        if (!(o instanceof SmellPoint)) { // StackOverflowError
            return o.equals(this);
        }

        // o가 ColorPoint이면 색상까지 비교한다.
        return super.equals(o) && this.smell == ((SmellPoint) o).smell;
    }
}

void test() {
    Point cp = new ColorPoint(2, 3, Color.RED);
    Point sp = new SmellPoint(2, 3, Smell.SWEET);

    System.out.println(cp.equals(sp));
}
  • 위와 같이 ColorPoint와 SmellPoint에 대해 equals비교를 한다면 !(o instanceof ColorPoint)에서 무한 재귀 호출이 일어날 것이다.

리스코프 치환 원칙 위반

  • 리스코프 원칙 : 부모 타입의 모든 메서드는 하위 타입에서도 잘 작동해야 한다.
public class Point{
  @Override public boolean equals(Object o) {
     if (o == null || o.getClass() != getClass())
       return false;
     Point p = (Point) o;
     return p.x == x && p.y == y;
  }
}
  • 위의 코드는 같은 구현 클래스 간 비교에서만 참을 반환하게 되고, 상속받는 하위 클래스에서는 거짓을 반환할 것이다.

  • 이와 같은 경우, 상속을 사용하지 않고 컴포지션을 활용해 해결할 수 있다.

    • 상속에 의해 발생하는 대칭성 위배, 추이성 위배, 리스코프 치환 원칙이 일어나지 않을 것이다.
public ColorPoint {  
  private Point point;  
  private Color color;  
  
  public ColorPoint(int x, int y, Color color) {  
    this.point = new Point(x, y);  
    this.color = Objects.requireNonNull(color);  
  }  
  
  public Point asPoint() {  
    return this.point;  
  }  
  
  @Override  
  public boolean equals(Object o) {  
    if(!(o instanceof ColorPoint)) {  
      return false;  
    }  
    ColorPoint cp = (ColorPoint) o;  
    return this.point.equals(cp) && this.color.equals(cp.color);  
  }  
}

일관성

  • null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)를 반복해도 값이 변하지 않는다.

not null

  • null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false이다.
  • instanceof 를 통해 묵시적 null 검사를 해 주는 것이 좋다.

권장 구현 방법

  1. 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.
  2. instanceof 연산자로 올바른 타입인지 확인한다.
  3. 올바른 타입으로 형변환한다.
  4. 객체와 자기 자신의 대응되는 ‘핵심’ 필드들이 모두 일치하는지 하나씩 검사한다.

Leave a comment