Effective Java - Chapter 3

9 분 소요

Effective Java

[Effective Java 3/E] 3장 모든 객체의 공통 메서드

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

equals를 재정의하지 않아야 할 상황

  • 각 인스턴스가 본질적으로 고유하다.
  • 인스턴스의 논리적 동치성(logical equality)을 검사할 일이 없다.
  • 상위 클래스에서 재정의한 equals가 하위 클래스에도 딱 들어맞는다.
  • 클래스가 private이거나 package-private이고 equals 메서드를 호출할 일이 없다.

equals를 재정의해야 할 상황

  • 상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때
    • equals가 논리적 동치성을 확인하도록 재정의 해두면, 인스턴스 값의 비교는 물론 Map의 키와 Set의 원소로 사용할 수 있게 된다.

equals를 재정의할 때 따라야 할 규약

  • equals 메서드는 동치관계를 구현한다. (x, y, z는 null이 아님을 가정한다)
    • 반사성(Reflexivity): x.equals(x)는 true
    • 대칭성(Symmetry): x.equals(y)가 true면 y.equals(x)도 true
    • 추이성(Transitivity): x.equals(y)가 true고 y.equals(z)가 true면 x.equals(z)가 true

        public boolean equals(Object o) {
        	if(!(o instanceof Point))
        		return false;
        	//o가 일반 Point면 색상 무시하고 비교
        	if(!(o instanceof ColorPoint))
        		return o.equals(this);
        	//o가 colorPoint면 색상까지 비교
        	return super.equals(o) && ((CollorPoint o).color == color;
        }
      
      • ColorPoint로 생성된 p1객체와 Point로 생성된 p2객체가 색상을 제외한 값이 같아서 true를 출력하는 상황에서 p3객체에 색상값이 다르다면 false를 반환, 추이성 위배!
      • but, 구체 클래스를 확장해 새로운 값을 추가하면서 equals 규약을 만족시킬 방법은 없다
      • 상속 대신 컴포지션을 사용하여(Item 18) 우회할 수는 있다
        • Point를 ColorPoint의 private 필드로 두고 ColorPoint와 같은 위치의 일반 Point를 반환하는 뷰메서드(Item 6)를 public으로 추가
    • 일관성(Consistency): x.equals(y)는 항상 true를 반환 or 항상 false를 반환
    • null-아님: x.equals(null)은 false

양질의 equals 메서드 구현 방법

  • == 연산자를 사용해 입력이 자기 자신의 참조인지 확인. 자기 자신이면 true 반환
  • instance 연산자로 입력이 올바른 타입인지 확인
  • 입력을 올바른 타입으로 형변환
  • 입력 객체와 자기 자신의 대응되는 ‘핵심’ 필드들이 모두 일치하는지 하나씩 검사 #

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

  • OhashCode 관련 규약 중 2번째 조항

    equals(Object)가 두 객체를 다르다고 판단했더라도, 두 객체의 hashCode가 서로 다른 값을 반환할 필요는 없다. 단, 다른 객체에 대해서는 다른 값을 반환해야 해시테이블의 성능이 좋아진다.

    • hashCode를 잘못 재정의 했을 때 문제가 되는 부분이다. 즉, 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다
    • equals는 다른 객체를 논리적으로 같다고 할 수 있다. but hashCode 메서드는 이 둘이 다르다고 판단한다.
    • ∴ equals를 재정의한 클래스에서는 hashCode도 재정의!
  • 좋은 hashCode를 작성하는 간단한 요령

    1. int 변수 result를 선언한 후 값 c로 초기화한다. (2.a)
    2. 해당 객체 나머지 핵심 필드 f 각각에 다음 작업 수행

      a. 해당 필드 해시코드 c 계산

      b. a의 해시코드 c로 result를 갱신

      result = 31 * result + c;

    3. result를 반환한다.

#

Item 12. toString을 항상 재정의하라

toString을 잘 구현한 클래스는 사용하기 편리하고, 디버깅 하기 쉽다!

  • toString 재정의 요령
    1. toString은 그 객체가 가진 주요 정보를 모두 반환하는 게 좋다
    2. 주석을 처리하든 말든 의도를 명확하게 밝혀야 한다

       /**
       * 주절주절
       * 주절주절주절
       * ....
       * 주절주절
       */
      
       @override public String toString() {
       	return String.format("%03d-%03d-%04d",
       			areaCode, prefix, lineNum);
       }
      
       /**
       * 주절주절
       * 주절주절주절
       * ....
       * 주절주절
       */
      
    3. toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자.
      • 그렇지 않으면 개발자는 toString의 반환값을 파싱할 수 밖에 없다.
  • toString의 재정의가 필요 없는경우
    • 정적 유틸리티 클래스
    • 열거 타입(enum)
    • 상위 클래스에서 알맞는 toString을 제공한 경우 #

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

  • Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 인터페이스(mixin interface)이다. 하지만 의도한 목적에 조금 어긋난다. clone 메서드가 선언된 곳이 Cloneable이 아닌 Object 클래스이고 그마저도 protected 접근 지정자이다. Cloneable 인터페이스는 메서드도 하나 없지만, Object 클래스의 clone의 동작 방식을 결정한다.
  • Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.
  • clone 메서드의 허술한 일반 규약
    1. x.clone() != x 는 참이다. 원본 객체와 복사 객체는 서로 다른 객체이다.
    2. x.clone().getClass() == x.getClass() 는 참이다. 하지만 반드시 만족해야 하는 것은 아니다.
    3. x.clone().equals(x) 는 참이지만 필수는 아니다.
      • clone 메서드를 조작하는 경우 하위 클래스에서 의도하지 않은 결과를 받을 수 있음으로 주의해야 한다. 클래스 B가 클래스 A를 상속한다고 할 때, 하위 클래스인 B의 clone 메서드는 B 타입 객체를 반환해야 한다. 그런데 클래스 A의 clone이 자신의 생성자로 생성한 객체를 반환한다면, 클래스 B의 clone도 A 타입 객체를 반환할 수 밖에 없다. 그렇기 때문에 super.clone을 연쇄적으로 호출하도록 구현하면, 의도와 알맞는 객체를 만들 수 있다.
    • clone 재정의 시 참고사항
      1. clone 메서드는 사실상 생성자 효과를 낸다. clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
      2. Cloneable 아키텍처는 ‘가변 객체를 참조하는 필드는 final로 선언하라’는 일반 용법과 충돌한다.
        • final 필드에는 새로운 값을 할당할 수 없다
      3. public인 clone 메서드에서는 throws 절을 없애야 한다. 검사 예외를 던지지 않아야 메서드 사용이 편리하다.
    • clone 보다는 복사 생성자와 복사 팩터리를 사용한다

        public Yun(**Yum yum**) { ... };
        //복사 생성자
      
        public static Yum newInstance(Yum yum) {...};
        //복사 팩터리
      

      #

Item 14. Comparable을 구현할 지 고려하라

Comparable 인터페이스의 메소드는 compareTo 뿐이다. equals와 비슷하지만 동치성 비교에 더하여 순서까지 비교할 수 있으며, 제네릭하다. 알파벳, 숫자, 연대 같이 순서가 명확한 VO 클래스를 작성한다면 반드시 Comparable을 구현하자.

  • compareTo의 일반 규약 (≒equals)
    1. 이 객체와 주어진 객체의 순서를 비교한다.
    2. 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다.
    3. 이 객체와 비교할 수 없는 타입의 객체가 주어지면 ClassCastException을 던진다.
    4. 두 객체 참조의 순서를 바꿔서 비교해도 예상한 결과가 나와야 한다. (≒ 대칭성)
    5. 첫번째 객체가 두번째 객체보다 크고, 두번째 객체가 3번째 객체가 크면, 첫번째 객체는 세번째 객체보다 커야한다. (≒ 추이성)
    6. 크기가 같은 객체들 끼리는 어떤 객체와 비교하더라도 항상 같아야 한다.
    7. (권장) compareTo의 동치성 결과가 equals와 같아야 한다.
  • compareTo 메서드 작성 요령
    • compareTo 메서드의 인수 타입은 컴파일타임에 정해진다. 따라서 입력 인수의 타입을 확인하거나 형변환할 필요가 없다. (잘못됐다면 컴파일 자체가 불가)
    • 동치인지를 비교하는 게 아니라 순서를 비교한다
    • 객체 참조 필드를 비교하려면 compareTo 메서드를 재귀적으로 호출한다.
    • Comparable 구현하지 않은 필드에서는 Comparator를 대신 사용한다.
    • 실수 타입 필드 비교시, 자바 7부터는 <와>를 사용하지 않고 compare를 사용한다.
    • 핵심 필드부터 비교해나간다.

        public int compareTo(PhoneNumber pn) {
        	int result = Short.compare(areaCode, pn,areaCode);
        	if(result == 0) { //핵심부터
        		result = Short.compare(prefix, pn.prefix);
        		if(result == 0) {
        			....
        		}
        	}
        	return result;
        }
      
  • 비교자 생성 메소드(자바 8)

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

댓글남기기