EffectiveJava

[EffectiveJava] 3장 모든 객체의 공통 메서드

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

@Override
public boolean equals(Object o) {
    throw new AssertionError(); // 호출 금지
}
// 10-1 잘못된 코드 - 대칭성 위배!
public final class CaseInsensitiveString{
	private final String s;

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

	// 대칭성 위배
	@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";
List<CaseInsensitiveString> list = new ArrayList<>();
list.add(cis);
@Override public boolean equals(Object o){
		return o instanceof CaseInsensitiveString &&
				((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
public 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;
	}
	... // 나머지 코드는 생략
}
public class ColorPoint extends Point{
	private final Color Color;

	public ColorPoint(int x, int y, Color color){
		super(x,y);
		this.color = color;
	}
	...// 나머지 코드는 생략
}
// 10-2 잘못된 코드 - 대칭성 위배!
@Override public boolean equals(Object o) {
		if (!(o instanceof ColorPoint))
		   return false;
		return super.equals(o) && ((ColorPoint) o).color == color;
}
Point p = new Point(1,2);
ColorPoint cp = new ColorPoint(1,2,Color.RED);
// 10-3 잘못된 코드 - 추이성 위배!
@Override
 public boolean equals(Object o) {
     if (!(o instanceof ColorPoint))
         return false;

     // o가 일반 point면 색상을 무시하고 비교한다.
     if (!(o instanceof ColorPoint))
         return o.equals(this);

     // o가 ColorPoint면 색상을 무시하고 비교한다.
     return super.equals(o) && ((ColorPoint) o).color == color;
 }
// 이 방식은 대칭성은 지켜주지만, 추이성을 깨버린다.
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p1 = new Point(1, 2);
ColorPoint p2 = new ColorPoint(1, 2, Color.BLUE);
// 10-4 잘못된 코드 - 리스코프 치환 원칙 위배
@Override public boolean equals(Object o){
	if (o == null || o.getClass() != getClass())
	        return false;
	Point p = (Point) o;
	return p.x == x && p.y == y;
}
// 단위 원 안의 모든 점을 포함하도록 unitCircle을 초기화한다.
private static final Set<point> unitCircle= Set.of(
		new Point(1,0), new Point(0,1),
		new Point(-1,0), new Point(0,-1)
);

public static boolean onUnitCircle(Point p){
		return unitCircle.contains(p);
}
public class CounterPoint extends point{
		private static final AtomicInteger conter = new AtomicInteger();

		public CounterPoint(int x,int y){
				super(x,y);
				counter.incrementAndGet();
		}
		public static int numberCreated() { return conter.get(); }
}
// 10-5 equals 규약을 지키면서 값 추가하기
public class ColorPoint {
  private final Point point;
  private final Color color;

  public ColorPoint(int x, int y, Color color) {
      point = new Point(x, y);
      this.color = Objects.requireNonNull(color);
  }

	/**
	* 이 ColorPoint의 Point 뷰를 반환한다.
	*/
  public Point asPoint() {
      return point;
  }

  @Override public boolean equals(Object o) {
      if(!(obj instanceof ColorPoint))
          return false;
      ColorPoint cp = (ColorPoint) o;
		  return cp.point.equals(point) && cp.color.equals(color);
  }
	... // 나머지 코드는 생략
}
// 명시적으로 null 검사(비권장)
@Override public boolean equals(Object o){
		if(o == null)
				return false;
		...
}
//instanceof로 묵시적으로 null 검사를 할 수 있음(권장)
@Override public boolean equals(Object o){
	if(!(obj instanceof MyType))
	      return false;
	MyType mt (MyType) o;
	...
}
// 10-6 전형적인 equals 메서드의 예
public final class PhoneNumber{
		private final short areaCode, prefix, lineNum;

		public PhoneNumber(int areaCode, int prefix, int lineNum) {
		    this.areaCode = rangeCheck(areaCode, 999, "지역코드");
		    this.prefix = rangeCheck(prefix, 999, "프리픽스");;
		    this.lineNum = rangeCheck(lineNum, 999, "가입자 번호")
		}
		private static short rangeCheck(int val, int max, String arg) {
		    if (val < 0 || val > max)
		        throw new IllegalArgumentException(arg + ": " + val);
		    return (short) val;
		}
		
		@Override public boolean equals(Object o) {
		    if(o == this)
		        return true;
		    if (!(o instanceof PhoneNumber))
		        return false;
		    PhoneNumber pn = (PhoneNumber)o;
		    return pn.lineNum == lineNum && pn.prefix == prefix 
							 && pn.areaCode == areaCode;
		}
		... // 나머지 코드는 생략
}
// 잘못된 예 - 입력 타입은 반드시 Object여야 한다!
@Override boolean equals(MyClass o) {
	...
}
// 여전히 잘못된 예 - 컴파일 되지 않음
@Override public boolean equals(MyClass o) {
	...
}

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

문제가 되는 상황

Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "제니");
// 11-1 최악의 (하지만 적법한) hashCode 구현 - 사용금지
@Override public int hashCode() { return 42; }

좋은 해시코드를 만드는 방법

result = 31 * result + c;
// 11-2 전형적인 hashCode 메서드
@Override public int hashCode() {
    int result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix);
    result = 31 * result + Short.hashCode(lineNum);
    return result;
}
// 11-3 한줄짜리 hashCode 메서드 - 성능이 살짝 아쉽다.
@Override public int hashCode() {
   return Objects.hash(lineNum, prefix, areaCode);
}
// 11-4 해시코드를 지연 초기화하는 hashCode 메서드 - 스레드 안정성까지 고려해야 한다.
private int hashCode; // 자동으로 0으로 초기화된다.

@Override public int hashCode() {
   int result = hashCode;
   if(result == 0){
		result = Short.hashCode(areaCode);
		result = 31 * result + Short.hashCode(prefix);
		result = 31 * result + Short.hashCode(lineNum);
		hashCode = result;
   }
   return result;
}

12. toString을 항상 재정의하라

/**
* 이 전화번호의 문자열 표현을 반환한다.
* 이 문자열은 "XXX-YYY-ZZZZ"형태의 12글자로 구성된다.
* XXX는 지역코드, YYY는 프리픽스, ZZZZ는 가입자 번호다.
* 각각의 대문자는 10진수 숫자 하나를 나타낸다.
*
* 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
* 앞에서부터 0으로 채워나간다. 예컨대 가입자 번호가 123이라면
* 전화번호의 마지막 네 문자는 "0123"이 된다.
* */
@Override public String toString() {
  return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
}
/**
  * 이 약물에 관한 대략적인 설명을 반환한다.
  * 다음은 이 설명의 일반적인 형태이나, 
	* 상세 형식은 정해지지 않았으며 향후 변경될 수 있다.
  * "[약물 #9: 유형=사랑, 냄새=테레빈유, 겉모습=먹물]" */
  @Override
  public String toString() {...}

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

// 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드
@Override public PhoneNumber clone() {
      try {
          return (PhoneNumber) super.clone();
      } catch (CloneNotSupportedException e) {
          throw new AssertionError(); // 일어날 수 없는 일이다.
      }
  }
public class Stack{
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // 다 쓴 참조 해제
        return result;
    }

    // 원소를 위한 공간을 적어도 하나 이상 확보한다.
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}
// 13-2 가변 상태를 참조하는 클래스용 clone 메서드
@Override public Stack clone(){
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e){
        throw new AssertionError();
    }
}
public class HashTable implements Cloneable  {
  private Entry[] buckets = ...;
  private static class Entry {
    final Object key;
    Object value;
    Entry next;

    Entry(Object key, Object value, Entry next) {
      this.key = key;
      this.value = value;
      this.next = next;
    }
  }
	... // 나머지 코드는 생략
}
// 13-3 잘못된 clone 메서드 - 가변 상태를 공유한다!
@Override public HashTable clone() {
    try {
      HashTable result = (HashTable) super.clone();
      result.buckets = buckets.clone();
      return result;
    } catch(CloneNotSupportedException e) {
      throw new Assertion();
    }
  }
// 13-4 복잡한 가변 상태를 갖는 클래스용 재귀적 clone 메서드
public class HashTable implements Cloneable  {
  private Entry[] buckets = ...;

  private static class Entry {
    final Object key;
    Object value;
    Entry next;
		
    Entry(Object key, Object value, Entry next) {
      this.key = key;
      this.value = value;
      this.next = next;
    }

		// 이 엔트리가 가리키는 연결 리스트를 재귀적으로 복사
		Entry deepCopy(){
				return new Entry(key,value,next == null ? null : next.deepCopy());
		}
  }

  @Override public HashTable clone() {
		try {
          HashTable result = (HashTable) super.clone();
          result.buckets = new Entry[buckets.length];
          for (int i = 0; i < buckets.length; i++) 
              if (buckets[i] != null)
                  result.buckets[i] = buckets[i].deepCopy();
          return result;
    } catch(CloneNotSupportedException e) {
		      throw new Assertion();
    }
  }
	... // 나머지 코드는 생략
}
// 13-5 엔트리 자신이 가리키는 연결 리스트를 반복적으로 복사한다.
Entry deepCopy() {
    Entry result = new Entry(key, value, next);
    for (Entry p = result; p.next != null; p = p.next) 
            p.next = new Entry(p.next.key, p.next.value, p.next.next);
    return result;
}
// 13-6 하위 클래스에서 Cloneable을 지원하지 못하게 하는 clone 메서드
@Override
protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
}
// 13-7 복사 생성자
public Yum(Yum yum) {..}
// 13-8 복사 팩터리
public static Yum newInstance(Yum yum) { ... }

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

Arrays.sort(a); // 쉽게 정렬할 수 있다.
public class WordList {
    public static void main(String[] args) {
        Set<String> s = new TreeSet<>();
        Collections.addAll(s, args);
        System.out.println(s);
    }
}
public interface Comparable<T> {
      int compareTo(T t);
  }
// 14-1 객체 참조 필드가 하나뿐인 비교자
public final class CaseInsensitiveString implements Comparable<CaseInsensiviveString> {
      public int compareTo(CaseInsensitiveString cis) {
          return String.CASE_INSENSITIVE_ORDER.comapre(s, cis.s);
      }
			... // 나머지 코드 생략
}
// 14-2 기본 타입 필드가 여럿일 때의 비교자
public int compareTo (PhoneNumber pn) {
		int result = Short.compare(areaCode, pn.areaCode); // 가장 중요한 필드
		if (result == 0) {
		    result = Short.compare(prefix, pn.prefix); // 두 번째로 중요한 필드
		    if(result == 0){
		        result = Short.compare(lineNum, pn.lineNum); // 세 번째로 중요한 필드
		    }
		}
		return result;
}
// 14-3 비교자 생성 메서드를 활용한 비교자
private static final Comparator<PhoneNumber> COMPARATOR = Comparator.comparingInt(
            (PhoneNumber pn) -> pn.areaCode)
            .thenComparing(pn -> pn.prefix)
          .thenComparing(pn -> pn.lineNum);

public int compareTo(PhoneNumber pn) {
    return COMPARATOR.compare(this, pn);
}
// 14-4 해시코드 값의 차를 기준으로 하는 비교자 - 추이성을 위배한다!
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
      public int compare(Object o1, Object o2) {
          return o1.hashCode() - o2.hashCode();
      }
  };
// 14-5 정적 compare 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = new Comparator<Object>() {
      @Override
      public int compare(Object o1, Object o2) {
          return Integer.compare(o1.hashCode(), o2.hashCode());
      }
};
// 14-6 비교자 생성 메서드를 활용한 비교자
static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());

Leave a Reply