Item10 - equals는 일반 규약을 지켜 재정의하라(1)

2023. 11. 20. 23:16Book/이펙티브 자바

 

3장. 모든 객체의 공통 메서드

 - Object는 객체를 만들 수 있는 구체 클래스지만 기본적으로는 상속해서 사용하도록 설계되었다.

Object 에서 final이 아닌 메서드(equals, hashCode, toString, clone, finalize)는 모두 재정의를 염두에 두고 설계된 것이라 재정의 시 지켜야 하는 일반 규약이 명확히 정의되어 있다.

 메서드를 잘못 구현하면 대상 클래스가 이 규약을 준수한다고 가정하는 클래스(HashMap, HashSet)를 오동작하게 만들 수 이다. 이번 장에서는 item8에서 다룬 finalize메서드를 제외하고 어떻게 공통 메서드를 재정의하는지를 다룬다.

 

 

 equals는 재정의 하지 않는 것이 최선!! 다음과 같은 경우는 재정의 할 필요가 없다.

 

 1. 각 인스턴스가 본질적으로 고유하다.(싱글톤, Thread, enum)

    => 비교할 대상이 자기밖에 없기 때문에

 

 2. 인스턴스의 '논리적 동치성'을 검사할 필요가 없다.

    => 값이 같은지 검사할 필요가 없는 경우

 

 3. 상위 클래스에서 재정의한 equals가 하위 클래스에도 적절하다.

 

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

    =>(당연한 말 같지만 public 일 경우는 외부에서 equals 비교를 하지 않는다는 보장이 없기 때문)

 

 

equals를 재정의 해야 하는 경우 일반 규칙을 지켜 재정의하라!

 equals 규약 : equals 메서드는 동치관계를 구현하며, 다음을 만족한다.

 1. 반사성: A.equals(A) == true

    => 나랑 내가 같아야 한다.

 

 2. 대칭성: A.equals(B) == B.equals(A)

    - CaseInsensitiveString

    => A랑 B가 같으면 B랑 A같아야 한다

 

 3. 추이성: A.equals(B) && B.equals(C), A.equals(C)

   - Point, ColorPoint(inherit), CounterPointer, ColorPoint(comp)

   => A랑 B가 같고, B랑 C가 같으면 A랑 C가 같아야 한다.

 

 4. 일관성: A.equals(B) == A.equals(B)

   => A랑 B가 

 5. null-아님: A.equals(null) == false

 

 

 

동치관계란 무엇일까? 쉽게 말해 집합을 서로 같은 원소들로 이뤄진 부분집합으로 나누는 연산이다. 이 부분집합을 동치류라고 한다.

 equals 메서드가 쓸모 있으려면 모든 원소가 같은 동치류에 속한 어떤 원소와도 서로 교환한 수 있어야 한다. 말이 어려울 수 있으니 예시를 살펴보자

 

1.반사성 : 쉽고 일부러 어기지 않는 한 어기기가 어려우므로 넘어간다.

 

2.대칭성은 두 객체는 서로에 대한 동치 여부에 똑같이 답해야 한다는 뜻이다. 반사성과는 달리 대칭성 요건은 자칫하면 어길 수 있다.

public final class CaseInsensitiveString {
    private final String s;

    public CaseInsensitiveString(String s) {
        this.s = Objects.requireNonNull(s);
    }

    //잘못된 equals
    @Override
    public boolean equals(Object o) {
        // 전달 받은 Object가 CaseInsensitiveString 타입일 때 필드 s 대소문자 구분 없이 비교
        if (o instanceof CaseInsensitiveString) {
            return s.equalsIgnoreCase(
                    ((CaseInsensitiveString) o).s
            );
        }
        // 전달 받은 Object가 String 데이터 타입일 때 대소문자 구분 없이 비교
        if (o instanceof String) {
            return s.equalsIgnoreCase((String) o);
        }
        return false;
    }


    public static void main(String[] args) {
        CaseInsensitiveString cis = new CaseInsensitiveString("Polish");
        String polish = "polish";
        System.out.println(cis.equals(polish));
        System.out.println(polish.equals(cis));

        List<CaseInsensitiveString> list = new ArrayList<>();
        list.add(cis);

        System.out.println(list.contains(polish));
    }
}


==>
true
false
false

 

cis.equals(polish)는 equals 메서드의 두번째 조건에 해당하여 대소문자 구분없이 true를 반환한다. 하지만 

polish.equals(cis)는 String 메서드의 equals 규약을 따르므로 당연히 CaseInsensitiveString 내부에 있는 필드 s를 꺼내와서 비교하지 않을 것이므로 false를 반환할 것이다.