본문 바로가기
책 정리/이펙티브 자바

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

by chanwoodev 2023. 6. 23.

equlas 메서드 재정의를 조심해야한다

 

다음과 같은 상황이 아니라면 재정의하지 않는 것이 최선이다

 

  • 각 인스턴스가 본질적으로 고유하다
  • 인스턴스의 '논리적 동치성'을 검사할 일이 없다
  • 상위 클래스에서 재정의한 equals가 하위 클래스에서도 딱 들어맞는다
  • 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다

equlas를 호출할 일이 없다면 아래처럼 구현한다

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

 

equals 재정의가 필요할 때는?

 

아래 사항을 만족해야한다

 

  • 논리적 동치성을 확인해야할 때
  • 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의 않았을 떄.
  • 값 클래스라면 값이 같은 인스턴스가 둘 이상 만들어 질 수 있을 떄 

 

equlas 메서드 재정의 일반 규약

 

동치 관계(equivalence relation)를 구현하며 다음을 만족한다

 

  • 반사성: null이 아닌 모든 참조 값 x에 대해, x.equals(x)는 true이다
  • 대칭성: 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이다.
  • 일관성: null이 아닌 모든 참조 값 x, y에 대해 x.equals(y)반복해서 호출하면 항상 true를 반환하거나 false를 반환한다
  • null-아님: null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false다.
규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할 지 알 수 없음

 

예시를 통한 위배 확인

예시) 상위 클래스 - 하위 클래스

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

    ...

    @Override public boolean equals(Object o) {
    	if (!(o intaceof Point))
        	return false;
        Point p = (Point)o;
        return p.x == x && p.y == y;
    }
}

 

위와 같은 클래스를 상속받아 확장한 클래스

public class ColorPoint extends Point {
    private final Color color;

    ...

}

 

위와 같은 구조가 있다고 했을 떄 

 

대칭성 위배 equals

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

ex) Timestamp의 경우 Date를 상속받아 구현되었으며 대칭성을 위반하는 사례이다

추이성 위배 equals

@Override public boolean equals(Object o) {
    if (!(o instanceof Point))
        return false
    
    // o가 일반 Point면 색상을 무시하고 비교
    if (!(o instanceof ColorPoint))
    	return o.equals(this);
        
    return super.equals(o) && ((ColorPoint) o).color == color;
}

위의 코드는 대칭성은 지키지만 아래 상황에서 추이성을 위배한다

ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);

p1.equals(p2) true

p2.equals(p3) true

p1.equals(p3) false

 

리스코프 치환 법칙 위배

추이성을 위배하지 않기 위해 Point 클래스에서 instanceof 대신 getClass를 사용해 하위 클래스와의 비교를 막아버린다면?

@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;
}

ColorPoint는 Point로서 쓰이지 못하게 됨

 

Set<Point> pointSet = new Hashset<>();

// pointSet.contain(ColorPoint)은 무조건 false

pointSet.contain(ColorPoint 인스턴스)같이 equals를 사용하는 함수에서는 무조건 false가 반환되기에 Point로서 기능을 하지 못함

 

리스코프 치환 원칙

상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

ColorPoint가 Point로서 활용되지 못하므로 리스코프 치환 원칙을 위배

구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않음

 

우회법 - 상속 대신 합성을 사용하자

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

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

 

위 방식처럼 우회를 한다면 대칭성, 추이성을 만족하며 리스코프 치환 원칙에도 위배되지 않는다

 

일관성 위배

equals의 판단에 신뢰할 수 없는 자원이 끼어들게 해서는 안 됨

 

null - 아님

  • 모든 객체가 null과 같지 않아야함
  • NullPointerException을 던지는 것도 일반 규약을 위반함
  • 많은 클래스가 null 보호를 함

instanceof 연산자는 첫 번째 피연산자가 null이면 false를 반환하기에 묵시적으로 null 검사를 할 수 있다

@Override public boolean equals(Object o) {
    if (!(o instanceof MyType))
        return false;
    MyType mt = (MyType) o;
   ,,,
}

 

 

올바른 equals 메서드 구현법

 

1. == 연산자를 사용해 입력이 자기 자신의 참조인지 확인한다.

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

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

4. 입력 객체와 자기 자신의 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.

 

 

기본 타입 연산자 비교

float, double 제외 기본 연산자 ==로 비교

float, double의 경우 ==가 아닌 Float.compare(float, float), 애 Double.compare(double, double)로 비교

 

 

주의사항

  • equals를 다 구현했으면 대칭성, 추이성, 일관성을 다시 검토하자
  • equals 단위 테스트를 작성하되 AutoValue같은 툴을 이용하자
  • equals 재정의할 땐 hashcode도 반드시 재정의하자
  • 필드의 동치성 검사이외에 deep한 검사를 조심하자 - 변경의 가능성이 있다
  • Object 외의 타입을 매개변수로 받는 equals는 선언하지 말자 - 해당 선언은 overloading 처리가 되며 @Overrinding을 붙혀 실수를 방지하자