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

지구🌍·2023년 2월 19일
0

Effective Java 공부

목록 보기
3/12
post-thumbnail

equals 메소드를 정의하기 생각보다 쉽지 않다. 재정의하지 않는 것이 최선이지만 재정의가 필요한 상황에서는 아래 가이드에 따라 정의해보자!

equals 재정의하지 않아야하는 경우

1. 각 인스턴스가 본질적으로 고유한 경우

값을 표현하는 것 아니라 동작하는 개체를 표현하는 클래스일 경우

  • Thread
  • Object.equals()
  • Bean에 등록해두는 객체 repository, controller, service 가 이에 해당

2. 인스턴스의 논리적 동치성(logical equality)을 검사할 일이 없을 경우

Object.equals() 만으로도 해결이 가능한 경우

3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는 경우

  • 같은 특징을 가지기 때문에 equals를 상속받아 사용하는 것을 권장
  • Set, Map, List 의 경우 AbstractList로 부터 equals 상속 받아 사용
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
	public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof List))
            return false;

        ListIterator<E> e1 = listIterator();
        ListIterator<?> e2 = ((List<?>) o).listIterator();
        while (e1.hasNext() && e2.hasNext()) {
            E o1 = e1.next();
            Object o2 = e2.next();
            if (!(o1==null ? o2==null : o1.equals(o2)))
                return false;
        }
        return !(e1.hasNext() || e2.hasNext());
    }
}

위의 코드와 같이 List의 대부분은 equals 메소드가 재정의 되어있고 이걸 사용한다.

4. 클래스가 private 이거나 package-private 이고 equals 메소드를 호출할 일이 없는 경우

equals 가 실수로라도 호출되는 것을 막고 싶다면 아래와 같이 하는 것이 좋다

@Override
public boolean equals (Object object) {
	throw new AssertionError(); //equals 호출시 error()
}

5. 싱글톤이나 인스턴스가 둘 이상 만들어지지 않음이 보장되는 클래스

Enum

equals를 재정의 해야하는 경우

💡 객체 식별성 (메모리 상에 같은 위치에 잇는지)이 아니라 논리적 동치성을 확인해야하는데 사우이 클래스의 equals가 논리적 동치성 비교가 아닐 때 사용해야한다.

  • Integer, String 등의 값 클래스 (객체가 같은지가 아니라 값이 같은지를 알기 위해서)

equals 메소드 재정의를 위한 일반 규약(동치관계를 위한 조건)

💡 동치관계 : 집합을 서로 같은 원소들로 이루어진 부분집합으로 나누는 연산

1. 반사성 (reflexivity)

x.equals(x) == true
  • 객체는 자기 자신과 같아야한다라는 뜻
  • 확인 방법은 인스턴스가 들어있는 컬렉션에 contains 메소드를 호출해 true가 나오는지 확인

예시

public static void main(String[] args) {
	List<Member> members = new ArrayList<>();
	Member member = new Member("rutgo", 29);
	members.add(member);
    System.out.println(members.contains(member)); // true
}

2. 대칭성 (symmetry)

x.equals(y) == y.equals(x)
  • 두 객체는 서로에 대한 동치 여부에 똑같이 답해야한다는 뜻

예시

// CaseInsensitiveString 클래스
class CaseInsensitiveString {
	...
	@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("Polish");
String s = "polish";

cis.equals(s); // true
s.equals(cis); // false

String 클래스의 equals는 대소문자 구별하도록 구성되어있기 때문에 해당 부분은 대칭성을 명백히 위반
if(o instanceof String) ->
if(o instanceof CaseInsensitiveString && ((CaseInsensitiveString) o).s.equalsIgnoreCase(s)

이렇게 변경하면 대칭성을 위반하지 않을 수 있다.

3. 추이성 (transitivity)

x.equals(y) == true
y.equals(z) == true
x.equals(z) == true
  • 첫번째, 두번째 객체가 같고 두번째, 세번째 객체가 같다면 첫번째 객체와 세번째 객체도 같아야한다는 뜻
  • 상위 클래스에서 equals를 재정의했을 경우 equals를 재정의하면 안된다.

예시

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 p.x == x && p.y == y;
	}
}

해당 Point 객체를 확장하여 color라는 필드를 추가한다면 어떤 상황이 벌어질까?

이러한 상황이라면 색상 정보는 무시한 채 부모 클래스인 Colorequals()를 사용하여 비교하게 된다.
대칭성이 깨진다.

class ColorPoint extend Point {
	private final Color color;
    
    public ColorPoint (int x, int y, Color color) {
    	super(x, y);
        this.color = color;
    }
    
    @Override
	public boolean equals(Object o) {
		if(!(o instanceof Point))
			return false;
        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

대칭성이 깨지는 상황을 막기 위해 ColorPoint 까지 확인하도록 equals()를 수정하면 이 부분에서 추이성을 위반하게 된다.
또한, 무한 재귀에 빠질 위험도 있다.

❗ 구체클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 존재하지 않는다.
그러나, 상속 대신 컴포지션을 사용하면 만족할 수 있다.
추상 클래스의 하위 클래스에서는 equals 규약을 지키면서 값을 추가 할 수 있다.

4. 일관성 (consistency)

x.equals(y) == true;
x.equals(y) == true;
x.equals(y) == true;
  • 불변
  • 클래스가 불변이든 가변이든 equals의 판단에 신뢰할 수 없는 자원이 끼워들어서는 안된다.

5. non-null

x.equals(null) == false
  • 모든 객체가 null과 같지 않아야한다는 뜻
  • 추가적으로 NullPointException을 던지는 경우조차 허용하지 않는다.

예시

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

    return email.equals(member.email) && name.equals(member.name) && age.equals(member.age);
}

위의 예시와 같이 equals() 재정의 시 타입 검사를 하게 된다면 쉽게 지킬 수 있다.

equals 메소드 단계별 구현 방법

  1. == 연산자를 사용하여 입력이 자기 자신의 참조인지 확인한다.
    • 단순한 성는 최적화용으로 비교 작업이 복잡한 상황일 때 값어치를 한다.
  2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
    • non-null 체크 가능
    • 특정 인터페이스 타입 체크 가능 (ex. Collection)
  3. 입력을 올바른 타입으로 형변환한다.
    • 2번의 instacneof 검사를 했기 때문에 100% 성공한다.
  4. 입력 객체와 자기 자신의 대응되는 핵심 필드들이 모두 일치하는지 하나씩 검사한다.

주의사항

  1. 기본 타입은 == 으로 비교하고 그 중 double, floatDouble.compare(), Float.compare()을 이용해 검사해야 한다.
  2. 배열의 모든 원소가 핵심 필드이면 Arrays.equals 메서드들 중 하나를 사용하자.
  3. 재정의시 hashCode도 반드시 재정의하자.
  4. 꼭 필요할 때만 재정의하자.
  5. Object 외의 타입을 매개변수로 받는 equals 메서드는 선언하지 말자. 이는 재정의가 아니라 다중 정의한 것이다.

🎈귀중한 참고자료🎈
참고자료1
참고자료2

profile
일취월장 하며 성장! 중! 공부한 것을 기록하자(^∀^●)ノシ

0개의 댓글