ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] Effective Java 3/E 정리 - 3장 모든 객체의 공통 메서드
    Study/이펙티브 자바 2021. 6. 16. 18:55
    반응형

     

    3장은 Object에서 재정의가 가능한 메서드를 재정의할 때 지켜야할 규약에 대해 설명한다.

    * equals, hashCode, toString, clone, finalize

    규약을 지키지 않고 잘못 구현할 경우 HashMap, HashSet 등과 같은 클래스에서 오동작할 위험이 있다.

     


    Item10. equals는 일반 규약을 지켜 재정의하라

    equals를 재정의하지 않고 사용할 경우 오직 자기 자신과만 같게 된다.

    아래 4가지의 상황에 해당된다면 재정의하지 않는 것이 좋다.

    • 각 인스턴스가 본질적으로 고유하다. 값을 표현하는 것이 아니라 동작하는 객체를 표현하는 클래스가 해당한다. ex) Thread
    • 인스턴스의 '논리적 동치성(logical equality)'을 검사할 일이 없다.
    • 상위 클래스에서 재정의한 equals가 하위 클래스에도 해당된다. ex) AbstractSet → Set 구현체, AbstractList → List 구현체, ...
    • 클래스가 private거나 package-private고 equals 메서드를 호출할 일이 없다. 혹시라도 equals를 호출하는 위험을 막고 싶다면 AssertionError를 던지자.
      @Override
      public boolean equals(Object o) {
      		throw new AssertionError();
      }​

    equals를 재정의해야 하는 상황은 논리적 동치성을 확인할 필요성이 있을 때이다.

    이때 상위 클래스의 equals를 통해서도 비교되지 않는다면 재정의해야 한다.

     

    다음은 Object 명세에 적힌 equals 메서드를 정의할 때 지켜야할 규약이다. equals 메서드는 동치관계(equivalence relation)를 구현하며, 다음을 만족한다.

    • 반사성(reflexivity): null이 아닌 모든 참조 값 x에 대해 x.equals(x)는 true이다.
      → 단순하게 말하자면 객체는 자기 자신과 같아야 한다는 뜻이다.
    • 대칭성(symmetric): null이 아닌 모든 참조 값 x,y에 대해 x.equals(y)가 true면 y.equals(x)도 true이다.
      → 두 객체를 서로를 각각 비교했을 때 같은 값을 반환해야 한다.
    • 추이성(transitivity): null이 아닌 모든 참조 값 x,y,z에 대해 x.equals(y)가 true, y.equals(z)가 true이면 x.equals(z)도 true이다
      → 삼단논법. A가 B와 같고, B가 C와 같으면 A는 C와 같다.
      → 상속시 쉽게 어길 수 있는데, 새로운 필드를 추가하면서 equals 규약을 만족할 수 있는 방법은 없다. → 그래서 상속 대신 private 필드로 두고, 이를 반환하는 메서드를 두는 방법으로 우회할 수 있다.
    • 일관성(consistency): null이 아닌 모든 참조 값 x,y에 대해, x.equals(y)를 반복해서 호출하면 항상 true를 반환하거나 항상 false를 반환해야한다.
      → 수정되지 않는 한 영원히 같아야한다.
    • null이 아닌 모든 참조 값 x에 대해 x.equals(null)은 false이다.

    이를 지키기 위한 equlas 메서드 구현 방법은 아래와 같다.

    1. == 연산자를 이용해 입력이 자기 자신의 참조인지 확인한다. (단순한 성능 최적화용)
    2. instanceof 연산자로 입력이 올바른 타입인지 확인한다.
    3. 입력을 올바른 타입으로 형변환한다. (3번을 거치면 성공!)
    4. 피연산자와 대응되는 '핵심' 필드들이 모두 일치하는지 하나씩 검사한다.
      → 하나라도 다르면 false. 다를 확률이 높은 것부터 검사한다.
      → 기본 타입 필드는 ==, 참조 타입은 equals, float와 double은 Float 혹은 Double의 compare메서드를 사용한다.
      → null을 정상 값으로 취급하는 필드는 Objrcts.equals(Object, Object)를 이용하자.

     

    Item11. equals를 재정의하려거든 hashCode도 재정의하라

    euals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다.

    equals를 재정의하고 hashCode를 재정의하지 않는다면,

    equals가 두 객체를 같다고 판단했다면, 두 객체의 hashCode는 같은 값을 반환해야 한다

    라는 Objcet 명세의 조항에 위배될 위험이 있다.

    이 조항에서 위배된다면 HashMap, HashSet과 같은 컬렉션들은 HashCode가 다르면 값을 비교를 하지 않기 때문에 같은 값이 여러번 들어가는 오류를 일으킨다.

     

    그러면 hashCode를 어떻게 재정의해야 할까?

    책에서는 3단계에 거쳐 hashCode를 생성하지만 굳이 풀어쓰기에 길기때문에 p.68을 참고하자.

     

    Item12. toString을 항상 재정의하라

    toString은 기본적으로 클래스이름@해시코드를 반환한다.

    toString 메서드는 디버깅시 로그를 남길 때 많이 사용되는데, 따라서 메서드를 간결하고 사람이 읽기 쉬운 형태로 재정의하는 것이 좋다.

     

    재정의할 때는 가진 주요 정보를 모두 반환하는 것이 좋고, 포맷을 결정했다면 그 의도를 명확히 해야한다.

    포맷을 명시했다면 추후에 이에 맞춰 사용해야한다는 것(유연성 저하)을 주의하자.

    또한 toString을 파싱하여 사용하지 않도록 toString에 포함되는 값은 반환값을 얻을 수 있는 API를 제공하는 것이 좋다.

     

    Item13. clone 재정의는 주의해서 진행하라

    Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스이다.

    Objcet의 protected메서드인 clone의 동작 방식을 결정하여 Cloneable을 구현한 클래스는 clone을 호출시 복사한 객체를 반환하고, Cloneable을 구현하지 않은 클래스는 CloneNotSupportedException을 던진다.

     

    문제는 clone을 구현한 클래스의 하위 클래스를 만들 때이다.

    super.clone()을 통해 필드를 copy 후 가변 필드들은 deep copy하여 복사본을 만들고,

    참조가 필요한 필드의 참조를 복사본으로 가르키게 변경해야한다.

     

    이는 복잡하고 오류를 발생시키기 쉽기 때문에 Cloneable는 final 클래스에서 사용하는 것이 좋다.

    만약 clone 메서드가 필요하다면, 복사 생성자나 복사 팩터리를 사용하는 것이 더 좋은 방법이다.

     

    Item14. Comparable을 구현할지 고려하라

    Comparable 인터페이스는 compareTo 메서드를 제공하며,  이 메서드는 equals와 동일하게 동치성 비교를 해준다.

    다른 점으로는 순서까지 비교할 수 있고 제네릭하다는 특성을 갖는다.

    * 순서를 비교할 수 있기 때문에 Arrays.sort() 와 같은 방법으로 손쉽게 정렬 가능하다.

     

    띠라서 알파벳, 숫자, 연대와 같이 순서가 명확한 값 클래스는 Comparable클래스를 구현하는 것이 좋다.

    Comparable의 compareTo 메서드에 대한 규약은 equals 규약과 비슷하다.

    * 자세한 내용은 여기에서 확인하자.

     

    compareTo 규약을 지키지 못하면 비교를 활용하는 클래스를 제대로 사용할 수 없다.

    예를 들어 정렬된 컬렉션인 TreeSet, TreeMap이나 정렬 알고리즘을 쓰는 유틸리티 클래스 Collections, Arrays 등이 있다.

     

    compareTo 메서드를 작성하는 방법은 규약과 마찬가지로 equals와 비슷하고, 몇가지만 주의하면 된다.

    • 순서를 비교하기 때문에 관계 연산자 >, <를 사용시 오류를 유발할 수 있다.
      대신에 박싱된 기본 타입 클래스의 compare 메서드를 이용하자.
    • 클래스의 핵심 필드가 여러개 라면 중요한 순서대로 비교하자.
    • hashCode 값의 차를 이용한 비교는 정수 오버플로 혹은 IEEE 754 부동소수점 계산 방식에 따른 오류가 발생할 수 있다. Integer.compar 혹은 Comparator.comparingInt를 사용하자.

     

    비교자 생성 메서드

    비교자 생성 메서드를 이용하면 메서드 연쇄 방식으로 간결하게 구현 가능하지만, 약간의 성능저하가 뒤따른다.

    아래는 comparingInt와 thenComparingInt를 이용하여 구현한 예제이다.

    private static final Comparator<PhoneNumber> COMPARATOR =
        comparingInt((PhoneNumber pn) -> pn.areaCode)
        	.thenComparingInt(pn -> pn.prefix)		 
        	.thenComparingInt(pn -> pn.lineNum);
    
    public int compareTo(PhoneNumber pn) {
        return COMPARATOR.compare(this, pn);
    }

     

    Comparator는 기본 타입들의 비교자 생성 메서드를 지원하고, 객체 참조용 비교자 생성 메서드는 comparing과 thenComparing을 사용하면 된다.

    댓글