본문 바로가기

자바☕/이펙티브 자바

이펙티브 자바 읽고 정리해보기 10-1

728x90

3 장 : Obejct의 메서드, 모든 객체의 공통 메서드

 

아이템 10 : equals는 일반 규약을 지켜 재정의하라

 

equals 메서드는 다음과 같은 상황일 때는 재정의하지 않는 것이 좋다.

- 각 인스턴스가 본질적으로 고유하다

값을 표현하는 게 아니라 동작하는 개체를 표현하는 클래스들이 그 예시로(Thread), Object 클래스의 equals가 이에 적합하게 구현되어 재정의할 필요가 없다

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

java.util.regex.Pattern 클래스는 equals를 재정의하여 두 Pattern의 인스턴스가 같은 정규표현식을 참조하는지 논리적 동치성을 검증하기 위해 equals 메서드를 재정의했다. 다만 설계자에 따라 이 방식이 불필요하다고 여겨질 경우에는 재정의할 필요는 없다

- 상위 클래스에서 재정의한 equals가 하위 클래스에도 적합하다

대부분의 Set 구현체는 AbstractSet가 구현한 equals를 상속받아 사용하며, List 구현체는 AbstractList, Map 구현체는 AbstractMap에서 상속받아 사용한다. 재정의할 필요가 없다

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

실수로라도 eqauls가 호출되는 것을 원하지 않을 경우 재정의하되, 오류를 발생시키는 방식으로 일반적인 equals 메서드 호출을 막을 수 있다

@Override
public boolean equals(Object o) {
	throw new AssertionError();
}

 

equals의 재정의가 필요한 경우는, 객체 식별성(두 객체가 물리적으로 같은가)가 아니라 논리적 동치성을 확인해야 하나, 상위 클래스의 equals가 이에 적합하게 재정의되어 있지 않는 경우다.

주로 Integer와 String 같은 값 클래스에 해당하며, 개발자는 객체가 같은 지가 아니라 값이 같은 지를 알고 싶기 때문에 equals를 사용한다. 이 재정의를 통해, equals를 통해 검증한 인스턴스는 Map의 키와 Set의 원소로도 사용할 수 있다

 

값 클래스인 경우에도, 값이 같은 인스턴스가 둘 이상 만들어지지 않는 것이 보장된 경우(아이템 1)에는 equals를 재정의할 필요가 없다. 이 경우, 객체 식별성과 논리적 동치성이 같은 의미이기 때문이다

 

재정의할 때, 일반 규약을 준수해야 한다

-> equals 메서드는 동치 관계를 구현하며, 다음을 만족한다

1) 반사성 : null이 아닌 모든 참조 값 x에 대해 x.equals(x)는 true다.

2) 대칭성 : null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)가 true인 경우 그 반대도 성립한다

3) 추이성 : null이 아닌 모든 참조 값 x,y,z에 대해 x.equals(y)가 true이고 y.equals(z)일 때, x.equlas(z)도 성립한다

4) 일관성 : nuill이 아닌 모든 참조 값 x,y에 대해 x.equals(y)를 반복 호출해도 항상 같은 결과를 반환한다

5) null-아님 : null이 아닌 모든 참조 값 x에 대해, x.equals(null)은 false이다

 

Object 명세의 동치 관계란, 집합을 서로 같은 원소로 이루어진 부분 집합으로 나누는 연산이다. 이 부분 집합을 동치류(동치 클래스)라고 하며, 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환이 가능해야 한다

 

1) 반사성의 경우, 객체는 자기 자신과 같음이 보장되어야 한다는 뜻이다

 

2) 대칭성의 경우, 서로에 대한 동치 여부에 같은 결과를 반환해야 한다. 대소문자의 차이가 있는 String을 비교할 때, 이 클래스의 ToString은 서로 같은 결과를 반환하나, Object의 equals는 이 문자열에 대해 대소문자를 무시하기 때문에  대칭성을 보장하지 못한다

 

3) 추이성은 첫번째 객체와 두번째 객체가 같으면, 첫번째와 세번째 객체도 같다는 뜻이다.

public class Point {
	private final int x;
    private final int y;
    
    // 생성자 생략
    
    @Override
    public boolean equals(Object o) {
    	if (!o instanceof Point) return false;
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
}

Point 클래스

 

public class ColorPoint extends Point {
	private final Color color;
    
    public ColorPoint(int x, int y, Color color) {
    	super(x,y);
        this.color = color;
    }
}

이를 확장한 ColorPoint 클래스

 

이 경우, ColorPoint의 equals를 호출하면 color 정보에 대한 논리적 동치성을 확인하지 않는다(무시한다).

ColorPoint에서 equals를 재정의해 상위 클래스 조건에 Color 검증을 추가할 수 있으나, 이는 대칭성을 위배한다

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

 

대칭성을 지키기 위해 다음 코드를 추가하면 추이성이 깨진다

if ((!o instanceod ColorPoint)) return o.equals(this);

-> p1(ColorPoint)와 p2(Point)가 같으나, p3(ColorPoint)에서 color 값이 p1과 다른 경우에도 true이므로 추이성이 위배되는 equals 메서드가 된다

 

객체지향 추상화를 포기하지 않는 한, 구체 클래스를 확장해 새로운 값을 추가하며 equals 규약을 만족시킬 수 있는 방법은 없다

instanceof 대신 getClass 검증을 통해 시도해볼 수 있지만, 이는 리스코프 치환 원칙을 위배한다

-> 어떤 타입에 있어 중요한 속성인 경우, 그 하위 타입에서도 중요하다는 원칙

 

Point 타입의 Set 반환 인스턴스 unitCircle가 있고 정적 메서드를 통해 인스턴스에 Point가 포함되는 지를 판단하는 사례가 있다.

리스코프 치환 원칙을 준수하는 경우 Point의 하위 클래스는 여전히 Point로 활용될 수 있어야 하지만, getClass로 구현한 equals를 가진 경우, Set 클래스(그 외 다른 컬렉션 클래스도 동일)의 equals로 논리적 동치성을 확인할 때, Point를 상속한 다른 하위 클래스의 인스턴스와 Point의 인스턴스는 같은 값을 반환할 수 없다

 

이를 우회하는 방법으로는, "상속대신 컴포지션을 활용하라"(아이템 18)이 있다. 상속 대신 상위 클래스를 기존 하위 클래스의 필드로 두고, 해당 클래스와 같은 위치를 반환하는 상위 클래스의 view 메서드(아이템 6)를 구현하는 것이다

 

 

728x90