모든 객체의 공통 메서드


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

equals는 재정의하기 쉬워 보이지만 실수하면 결과는 끔찍하다.
문제를 회피하는 가장 쉬운 방법은 아예 재정의하지 않는 것이다.
다음 상황 중 하나에 해당한다면 재정의하지 않는 것이 최선이다.


equals를 재정의해야 할 떄는 언제인가?

객체 식별성(object identity, 물리적으로 같은가)이 아니라 논리적 동치성을 확인해야 하는데,
상위 클래스의 equals가 논리적 동치성을 비교하도록 재정의되지 않았을 때다.
주로 값을 표현하는 Integer와 String이 해당된다.


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

위 규약을 어기면 그 객체를 사용하는 다른 객체들이 어떻게 반응할지 알 수 없다.




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

equals를 재정의한 클래스 모두에서 hashCode도 재정의해야 한다.
그렇지 않으면 hashCode 일반 규약을 어기게 되어 해당 클래스의 인스턴스를
HashMap이나 HashSet 같은 컬렉션의 원소로 사용할 때 문제를 일으킬 것이다.

다음은 Object 명세의 규약이다.


hashCode 재정의를 잘못했을 때 크게 문제되는 조항은 두 번째다.
즉, 논리적으로 같은 객체는 같은 해시코드를 반환해야 한다.




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

Object의 기본 toString은 클래스_이름@16진수로_표시한_해시코드를 반환한다.
equals와 hashCode 규약만큼 대단히 중요하진 않지만,
toString을 잘 구현한 클래스를 사용한 시스템은 디버깅하기 쉽다.




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

Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스(mixin interface)이다.
아쉽게 의도한 목적을 제대로 이루지 못했다.

가장 큰 문제는 clone 메서드가 선언된 곳이 Cloneable이 아니라 Object이고,
protected에 있다는 것이다. 그래서 Cloneable을 구현하는 것만으로는 외부 객체에서
clone 메서드를 호출할 수 없다.

메서드가 하나도 없는 Cloneable 인터페이스는 무슨일을 할까?

인터페이스를 구현한다는 것은 일반적으로 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위다.
그런데 Cloneable의 경우 상위 클래스에 정의된 (Object)protected 메서드인 clone의 동작 방식을 결정한다.

실무에서 Cloneable을 구현한 클래스는 clone 메서드를 public으로 제공하며
사용자는 복제가 제대로 이뤄지리라 기대한다.
이 기대를 만족시키려면 그 클래스와 모든 상위 클래스는 복잡하고 강제할 수 없고
허술하게 기술된 프로토콜을 지켜야만 하는데 그 결과로 깨지기 쉽고 위험하며 모순적인 매커니즘이 탄생한다.
생성자를 호출하지 않고도 객체를 생성할 수 있게 되는 것이다.

clone 메서드의 일반 규약은 허술하다.
다음 식들은 일반적으로 참이지만, 필수는 아니다.

관례상 이 메서드가 반환하는 객체는 super.clone을 호출해 얻어야 하다.
이 관예를 다른다면 다음 식도 참이다.


가변 상태를 참조하지 않는 클래스용 clone 메서드

@Override 
public PhoneNumber clone() {
  try {
    return (PhoneNumber) super.clone();
  } catch (CloneNotSuppoertedException e) {
    throw new AssertionError();
  }
}

위 코드는 clone 메서드를 가진 상위 클래스를 상속해 Cloneable을 구현한 코드다.
위 clone 메서드가 동작하게 하려면 PhoneNumber의 클래스 선언에 Cloneable을 구현한다고 추가하면 된다.
그러나 이 코드는 클래스가 가변 객체를 참조하는 순간 재앙으로 돌변한다.


가변 상태를 참조하는 클래스용 clone 메서드

@Override 
public Stack clone() {
  try {
    Stack result = (Stack) super.clone();
    result.elements = elements.clone();
    return result;
  } catch (CloneNotSupportedException e) {
    throw new AssertionError();
  }
}

clone 메서드가 단순히 super.clone의 결과를 그대로 반환한다면 어떻게 될까?
반환된 Stack 인스턴스의 size 필드(int size)는 올바른 값을 갖겠지만,
elements 필드(Object[] elements)는 원본 Stack 인스턴스와 똑같은 배열을 참조할 것이다.
원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해친다.

Stack 클래스의 생성자를 호출한다면 이러한 상황은 일어나지 않는다.
clone 메서드는 사실상 생성자와 같은 효과를 낸다.
clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.

Stack의 clone 메서드가 제대로 동작하게 하려면 스택 내부 정보를 복사해야 하는데
가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출해주는 것이다.

한편 elements 필드가 final이었다면 위 방식은 작동하지 않는다.
이는 근본적인 문제로 직렬화와 마찬가지로 Cloneable 아키텍처는 ‘가변 객체를 참조하는 필드는 final로 선언하라’는 일반 용법과 충돌한다.
복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다.

clone을 재귀적으로 호출하는 것만으로는 충분하지 않을 때도 있다.
해시테이블 내부는 버킷들의 배열이고, 각 버킷은 키-값 쌍을 담는 연결 리스트의 첫 번째 엔트리를 참조한다.

다음은 Stack에서처럼 단순히 버킷 배열의 clone을 재귀적으로 호출한 코드이다.

잘못된 clone 메서드 - 가변 상태를 공유한다.

@Override 
public HashTable clone() {
  try {
    HashTable result = (HashTable) super.clone();
    result.buckets = buckets.clone();
    return result;
  } catch (CloneNotSupportedException e) {
    throw new AssertionError();
  }
}

복제본은 자신만의 버킷 배열을 갖지만, 이 배열은 원본과 같은 연결 리스트를 참조하여
원본과 복제본 모두 예기치 않게 동작할 가능성이 있다.
이를 해결하려면 각 버킷을 구성하는 연결 리스트를 복사해야 한다.

다음은 일반적인 해법이다.

복잡한 가변 상태를 갖는 클래스용 재귀적 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.deepCopy();
        }
      }
      return result;
    } catch (CloneNotSupportedException e) {
      throw new AssertionError();
    }
  }
}

Entry의 deepCopy 메서드는 자신이 가리키는 연결 리스트 전체 복사를 위해 자신을 재귀적으로 호출한다.
이 기법은 간단하지만 연결 리스트를 복제하는 방법으로는 그다지 좋지 않다.
재귀 호출 때문에 원소 수만큼 스택 프레임을 소비하며, 리스트가 길면 스택 오버플로를 일으킬 수도 있다.
이 문제를 피하려면 deepCopy를 재귀 호출 대신 반복자를 써서 순회하는 방법을 사용해야 한다.

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;
}


Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다.
그렇지 않은 상황에서는 복사 생성자와 복사 팩토리라는 더 나은 객체 복사 방식을 제공할 수 있다.

복사 생성자

public Yum(Yum yum) { ... };

복사 생성자란 단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자를 말한다.

복사 팩토리

public static Yum newInstance(Yum yum) { ... };


정리

새로운 인터페이스를 만들 때 Cloneable을 확장해서는 안 되며,
새로운 클래스도 이를 구현해서는 안 된다.
final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만
성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.
기본 원칙은 ‘복제 기능은 생성자와 팩토리를 이용하는 게 좋다.’라는 것이다.
단, 배열은 clone 메서드 방식이 가능 깔끔한, 규칙의 합당한 예외라 할 수 있다.




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

public interface Comparable<T> {
  int compareTo(T t);
}


정리

순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하여,
그 인스턴스들을 쉽게 정렬하고 검색, 비교 기능을 제공하는 컬렉션과 어우러지도록 해야 한다.

compareTo 메서드에서 필드의 값을 비교할 때 **<와> 연산자는 쓰지 말아야 한다.** 그 대신 박싱된 기본 타입 클래스가 제공하는 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.