ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • equals는 일반 규약을 지켜 재정의 하라
    JAVA/Effective java 2021. 1. 11. 13:41
    • equals 메서드는 재정의하기 쉬워 보이지만 자칫 잘못된 결과를 초래할 수 있다.
    • 따라서 가장 쉬운 길은 아예 재정의 하지 않는 것이다. 그냥 두면 그 클래스의 인스턴스는 오직 자신과만 같게 된다.

     

     

    따라서 다음 열거한 상황 중 하나에 해당되면 재정의하지 않는 것이 최선이다.

     


     

    1. 각 인스턴스가 본질적으로 고유할 때

    • 값을 표현하는 게 아닌 동작하는 개체를 표현하는 클래스, Thread 클래스는 하나의 좋은 예다.

     

    2. '논리적 동치성'을 검사할 일이 없을 때

    • Object에 기본 equals는 객체를 식별하기 위해 주소 값을 비교한다.
    • 만일 Pattern에 equals나 String에 equals 등 '논리적 동치성'의 경우가 없을 경우 재정의 하지 않아도 된다.

     

    3. 상위 클래스에 재정의한 equals가 하위 클래스에 들어맞을 때

    Set에 구현체중 하나인 HashSeet

     

    • Set의 구현체들은 AbstractSet, List에 구현체들은 AbstractList,
    • Map 구현체들은 AbstractMap 등 이들은 부모 클래스에 equals를 상속받아 그대로 쓰기에 재정의 할 필요가 없다.

     

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

     

    • throw new AssertionError() 등으로 호출을 금지 켜 위험을 회피할 수도 있다.

     

     

     

     

    그렇다면 eqauls를 재정의해야 할 때는 언제일까?

    • 객체 식별성이 아닌 논리적 동치성을 확인해야 하는데,
    • 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의 되지 않았을 때이다.
    • 주로 값 클래스들이 여기에 해당한다.

     

    • Integer나 String처럼 값을 표현하는 클래스들은 객체가 같은지가 아닌 값이 같은지를 알고 싶어 한다.
    • equals가 논리적 동치성을 확인하도록 정의해두면, 값을 비교할 수 있고 Map과 Set의 원소로 사용할 수 있게 된다.
    • 물론 값 클래스라 해도 같은 인스턴스가 둘 이상 만들어지지 않음을 보장한다면 재정의 하지 않아도 되며 Enum이 여기에 해당된다.

     

     

    규약


    • 세상에 홀로 존재하는 클래스는 없다. 한 클래스의 인스턴스는 다른 곳으로 빈번히 전달된다.
    • 그리고 컬렉션 클래스들을 포함해 수많은 클래스는 전달받은 객체가 eqauls 규약을 지킨다고 가정하고 동작한다.
    • 규약들에 관하여 알아보자.

     

    반사성

    • 객체는 자신과 같아야 한다.
    • 이 요건을 어긴다면 컬렉션에 넣은 다음 contains 메서드를 호출하면 인스턴스가 없다고 답할 것이다.

     

    대칭성

    • 서로에 동치 여부에 똑같이 답해야 한다. 다음의 예를 보자.

    • 이 예는 s.equals(cis)의 결과와 cis.equals(s)의 결과가 다르다.
    • 즉 cis String의 eqauls는 일반 String을 알아도 String의 equals는 cis의 존재를 모른다.
    • 이는 명백히 대칭성을 위반한다.

     

    • 따라서 cis의 equals를 String과도 연동하겠다는 허황된 꿈은 버려야 한다.

     

    추의성

    첫 번째 객체와 두 번째 겍체가 같고 두번째와 세번째 객체가 같다면 첫번째와 두번째 객체도 같아야 한다는 뜻이다.

     

    • Point 클래스를 확장해서 점에 색상을 더해보자.
    • equals 메서드는 어떻게 해야 할까?
    • equals를 그대로 두게 되면 Point 구현이 상속되어 색상 정보는 무시한 채 비교한다.

     

     

    • equals 규약은 어긴 것이 아니지만 중요한 정보를 놓치게 되니 받아들일 수 없다. 
    • 위치와 색상이 같을 때만 true를 반환하는 equals를 만들어보자.

     

    • 이 메서드는 일반 Point를 ColorPoint에 비교한 결과와 그 둘을 바꿔 비교한 결과가 다를 수 있다.
    • Point의 equals는 색상을 무시하고 ColorPoint의 equals는 입력 매개변수 클래스 종류가 다르기에 매번
    • false를 반환하게 된다.

     

     

    • p.equals(cp)는 true를 cp.equals(p)는 false를 반환한다.

     

     

    그렇다면 ColorPoint.equals가 Point와 비교 시에는 색상을 무시하도록 하면 될까?


    • 이 방식은 대칭성을 지켜주지만, 추이 성을 깨버린다.

     

    • 각각 true, true, false를 반환한다. p1과 p3비교는 색상까지 고려했기 때문이다.
    • 이 방식은 무한 재귀에 빠질 위험까지 도사린다.

     

     

    그렇다면 해법은?

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

    • 이번 equals는 같은 구현 클래스의 객체와 비교할 때만 true를 반환한다. 
    • 하지만 실제로 활용할 수는 없다.
    • Point의 하위 클래스는 정의상 여전히 Point이므로 어디서든 Point로써 활용될 수 있어야 한다.

    • 리스 코프 치환 원칙은 어떤 타입에 있어 중요한 속성이라면 그 하위 타입에서도 마찬가지로 동작해야 한다.
    • 따라서 그 타입의 모든 메서드가 하위 타입에서도 똑같이 잘 작동해야 한다.

     

    • 그런데 CounterPoint의 인스턴스를 onUnitCircle 메서드에 넘기면 어떻게 될까?
    • Point 클래스의 equals를 getClass를 사용해 작성했다면 false를 반환한다.
    • Set을 포함한 대부분의 컬렉션은 equals 메서드를 이용하는데 CounterPoint의 인 스터 스는 어떤 Point와도 같을 수
    • 없기 때문이다.
    • 반면 equals를 instanceOf 기반으로 올바로 구현했다면 CounterPoint 인스턴스 역시 제대로 동작할 것이다.

     

    • 구체 클래스의 하위 클래스에서 값을 추가할 방법은 없지만 괜찮은 우회 방법은 '상속 대신 컴포지션의 사용'이다.

    • Point를 상속 대신 Point를 ColorPoint의 private 필드로 두고 ColorPoint와 같은 위치의 일반 Point를
    • 반환하는 뷰 메서드를 public으로 추가하는 식이다.

     

    일관성

     

    • 두 객체가 같다면 영원히 같아야 한다는 뜻이다.
    • 불변 객체는 한번 다르면 끝까지 달라야 한다. 클래스를 작성할 때는 불변 클래스로 만드는 게 나을지 숙고하자.
    • 불변 클래스라면 equals가 한번 같다고 한 객체는 영원히 같아야 한다.

     

    • 따라서 equals의 판단에 신뢰할 수 없는 자원이 끼어서는 안 된다.

     

     

    equals 메서드의 구현을 정리해보자면


     

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

     

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

     

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

     

     

    • 다음에 따라 작성해본 PhoneNumber 클래스용 equals 메서드이다.

     

     

    • 이때 Object 외의 타입을 매개변수로 받는 equals 메서드를 선언하지 말자.
    • 타입을 구체적으로 명시한 equals는 해가 된다. 

     

     

    정리


    • 꼭 필요한 경우가 아니면 eqauls를 재정의하지 말자.
    • 많은 경우 Object의 equals가 원하는 비교를 정확히 수행해준다.
    • 재정의할 때는 이러한 규약들을 비교해가며 꼭 지켜서 재정의 하자.

     

     

    참고 자료 


    이펙티브 자바

Designed by Tistory.