[Effective Java] Item 10~11. equals는 일반 규약을 지켜서 재정의하라

2024. 3. 10. 17:03Java/Effective Java

반응형

1. equals 규약을 지키면서 값을 추가하기

equals를 override할 때에는 반드시 아래의 일반 규약을 만족해야 합니다.
(아래 규약은 모두 null이 아닌 참조값 x에 대한 규약입니다)

  • x.equals(x) 는 반드시 true 여야 하고
  • x.equals(y) == true 라면, y.equals(x) == true 여야 합니다.
  • x.equals(y) == true 이고, y.equals(z) == true 라면, x.equals(z) == true 여야 합니다.
  • x.equals(y) 결과는 여러번 호출하더라도 늘 같은 값을 반환해야 합니다.
  • x.equals(null) 은 반드시 false 여야 합니다.

위의 특징을 고려했을 때, 어떤 구현클래스의 상속클래스는 이 규칙을 만족하지 못하게 됩니다.

예를 들어, 아래와 같은 Point 객체가 있고,

@RequiredArgsConstructor
@Getter
public class Point {
    private final int x;
    private final int y;

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

이를 상속한 NamePoint 클래스가 있다고 할 때,

@Getter
public class NamePoint extends Point {
    private final String name;

    public NamePoint(int x, int y, String name) {
        super(x, y);
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if(o instanceof NamePoint p) {
            return super.equals(p) && this.name.equals(p.name);
        }
        return false;
    }
}

여기서 정의된 eqauls 메서드는 일반 규약을 만족하지 않습니다.
point를 namePoint와 비교한 경우에는 결과값이 true지만, 순서를 바꿀 경우 equals 결과가 달라지 기 때문입니다.

Point point = new Point(2, 3);
NamePoint namePoint = new NamePoint(2, 3, "coco");
System.out.println(point.equals(namePoint));  // true
System.out.println(namePoint.equals(point));  // false

객체 지향적 추상화의 이점을 이용하면서, 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 수 있는 방법은 없습니다

equals 규약을 지키면서 값을 추가하고 싶다면

  1. 구체 클래스의 하위 클래스에 값을 추가하는 것(상속)이 아닌, 컴포지션을 사용 하거나
  2. 최상위에 추상클래스를 정의하고, 하위클래스에서 값을 추가하면 됩니다.

composition 사용 예제

구체 클래스의 하위 클래스에 값을 추가하는 것(상속)이 아닌, 컴포지션을 사용 하면 equals 규약을 지키면서 값을 추가할 수 있습니다.

@Getter
@RequiredArgsConstructor
public class Point {
    private final int x;
    private final int y;

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

    public Point asPoint() {
        return point;
    }

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

또는, 최상위에 아무런 필드값을 갖지 않은 추상클래스로 정의하여 상위 클래스를 직접 인스턴스로 만드는게 불가능하다면 상속 개념을 이용하면서 equals 규약을 만족시킬 수 있습니다.


2. 기타 고려 사항

2.1. float, double 비교

부동소수 값을 다루는 float, double 타입의 비교는 정적메서드 Float.compare(f1, f2), Double.compare(d1, d2) 로 비교하여 equals 체크합니다.

Float.equals, Double.equals는 primitive 타입일 경우 auto boxing이 일어나기 때문에 성능이 더 떨어집니다.


2.2. null도 값으로 취급하는 참조타입 비교

null도 정상값으로 취급하는 참조타입이라면, Object.equals(o1, o2) 로 비교하여 NullPointerException 발생을 방지합니다.

또, 성능을 높이고 싶다면 가장 다를 가능성이 큰 필드부터 비교하도록 하면 좋습니다.

2.3. Object 이외의 타입을 매개변수로 받는 equals는 사용하지 말자

매개변수를 Object가 아닌 다른 값으로 넣는 것은, 기본 equals 매서드를 재정의하는 것이 아닌 동일한 메서드명으로 메서드를 overload 한 것입니다.

public boolean equals(Point p) { ... }

오버라이딩 할 메서드를 정의할 때, @Override 애너테이션을 붙여준다면, 애초에 컴파일시 에러가 발생되기 때문에 오류를 방지할 수 있습니다.

그러니, @Override 애너테이션을 붙여 실수를 예방합시다.

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

3. equals 재정의 후 hashCode도 재정의하라

equals를 재정의 한 후, hashCode를 재정의하지 않으면, HashMap같은 컬렉션에 요소를 넣을 때 문제가 발생됩니다.

두 객체의 equals 결과가 true라면 hashCode 결과도 같아야 합니다.

클래스가 불변 클래스면서 해시코드 계산 비용이 큰 경우라면, 지연 초기화 전략으로 hashCode를 읽어들이는 것도 좋은 방법입니다.

private int hashCode;

@Override
public int hashCode() {
	int result = hashCode;
	if(result == 0) {
		// 연산식
	}
	return result;
}

어느정도 성능도 고려하면서, 동일 객체 여부를 잘 판단하는 hashCode를 작성하는 것이 중요한데,
Lombok 을 이용하면 이런 부분을 매우 간편하게 해결할 수 있습니다.

@EqualsAndHashCode
@RequiredArgsConstructor
@Getter
public class Point {
    private final int x;
    private final int y;
}

지연 초기화 캐시전략도 지원합니다.
@EqualsAndHashCode(cacheStrategy = EqualsAndHashCode.CacheStrategy.LAZY)


++

  • 아이템 10. equals는 일반 규약을 지켜 재정의하라
  • 아이템 11. equals를 재정의하려거든 hashCode도 재정의하라
728x90
반응형