sungsu9022 / study-effective-java-3e

study for effective java 3 edition
1 stars 0 forks source link

이펙티브 자바 3판 - 3.모든객체의 공통메소드(item13) ~ 4. 클래스와 인터페이스 (item17) #7

Open genie-youn opened 5 years ago

genie-youn commented 5 years ago

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

Cloneable 은 복제해도 되는 클래스를 명시하는 Mixin Interface 이지만 의도한 목적을 제대로 이루지 못했음.

clone 메서드가 선언된것이 Object -> 재 정의를 강요할 수 없음

그마저도 protected 로 정의되어 있기 때문에 외부 객체에서 호출할 수 없음.

리플렉션을 사용하면 가능하지만 이 마저도 100%가 아니다.

그럼에도 불구하고 널리 쓰이니까 알아두면 좋음

clone 메서드가 잘 동작하게 하는 구현 방법과 언제 그렇게 해야하는지, 가능한 다른 선택지에 대해 논한다.

Cloneable 인터페이스는 놀랍게도 Objectprotected 메서드인 clone() 의 동작방식을 결정함.

Cloneable 을 구현한 클래스에서는 clone() 을 호출하면 그 객체의 필드들을 하나한 복사한 클래스를 반환하고 구현하지 않았을 경우에는 CloneNotSupportException 을 뱉는다.

이는 인터페이스를 굉장히 이례적으로 사용한 케이스니 따라하지 말것. 인터페이스를 구현한다는 것은 일반적으로 그 클래스가 인터페이스에서 정의한 기능을 제공한다는 의미지만 Cloneable 은 상위 클래스에 정의된 메서드의 동작방식을 변경한다.

!왜 이따위로 만들었지

Object 의 명세에서 가져온 clone 의 규약은 굉장히 허술함

객체의 복사본을 생성해 반환한다. 일반적으로 다음과 같은 규약을 따른다
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)

관습적으로 반환되는 객체는 super.clone() 을 호출하여 얻어야 한다. 만약 Object 를 제외한 모든 클래스와 슈퍼클래스가 이 규약을 지킨다면 x.clone().getClass() == x.getClass() 는 참이다.

관례상 반환된 객체와 원본 객체는 독립적이여야 한다. 이를 만족하려면 super.clone() 으로 얻은 객체의 필드중 하나 이상을 반환전에 수정해야 함. 강제성이 없다는 것을 제외하면 생성자 체인과 같다.

super.clone() 이 아닌 자기 자신의 생성자를 호출하면 하위 객체에서 clone() 을 호출했을 때 원하는 타입을 받지 못한다. 사실 말도 안되는게 super.clone() 으로 Object.clone() 의 동작방식에 기대지 않을 꺼면 Cloneable 을 구현할 필요 자체가 없다.

공변 반환 타이핑 covariant return type : 재정의한 메서드의 반환타입은 상위클래스가 반환하는 타입의 하위타입일 수 있다. super.~ 로 호출한 아이는 하위타입으로 캐스팅이 가능하다.

clone 의 구현이 가변 객체를 참조하면 재앙이 벌어짐 clone 메소드는 사실상 생성자와 같은 효과를 내므로 원본 객체에 아무런 영향을 끼치지 않는 동시에 객체의 불변식을 보장해야한다.

@Override public Stack clone() {
  try {
    Stack result = (Stack) super.clone();
    result.elements = elements.clone();
    return result;
  } catch (CloneNotSupportException e) {
    log.error(e);
    // re-throwing
  }
}

배열의 clone() 은 형변환이 필요없다. 런타임타입/컴파일타임타입 모두 원본의 타입을 반환하기 때문 (Object.clone() 이 아닌 Arrayclone() 을 사용한다.)

여기서 elementsfinal 이면 이 방법은 동작하지 않기 때문에 Cloneable 의 아키텍처는 '가변 객체를 참조하는 필드는 final 로 선언하라' 는 일반적인 용법과 충돌한다.

깊은 복사를 할때, clone 을 재귀적으로 호출하는 것만으로는 부족한 경우가 있음 (HashTable 의 clone 메소드) Entry // LinkedList 의 길이가 너무 길어지면 재귀호출 중 스택오버플로우가 발생, 이런 경우 순회로 깊은 복사를 구현할 것

또다른 방법은 super.clone() 으로 초기상태를 결정한 다음 원본 객체와 똑같은 상태로 만드는 고수준 API들을 만드는것이다. (같은 값으로 set 하는것)

생성자에서는 재정의 될 수 있는 메소드를 호출하지 말아야 하는데, clone 메서드에서도 마찬가지. 따라서 앞서 언급한 메서드는 final 이거나 private 일 것이다.

재정의한 값을 throw을 없앨것

상속용 클래스는 Cloneable을 구현하지 말것

!복사하는게 의미가 없어서? 하위클래스에게 위임하는게 맞아서?

근데 이런거 복잡하니까 복사 생성자 (변환 생성자) / 복사 팩터리 (변환 팩터리) 를 만드는게 더 낫다.

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

왜냐하면

  1. 언어모순적이고 위험천만한 (생성자를 사용하지 않는) 객체 생성메커니즘이 없고
  2. 엉성하게 문서화된 규약에 기대지도 않고
  3. 정상적인 final 용법과도 충돌하지 않으며
  4. 불필요한 checked-exception 을 뱉지도 않고
  5. 형변환도 필요없다.

더욱이 인터페이스를 인자로 받음으로써 유연한 변환도 제공할 수 있게 된다. 범용 컬렉션들이 Collection 이나 Map 을 인자로 받는 생성자를 사용하는 이유

결론

배열 외에 clone 은 쓰지마라

ITEM 14. Comparable 을 구현할 지 고려해라.

equals 와 비슷하지만 동치성 비교외에 순서까지 비교할 수 있다. Comparable 을 구현했다는 것은 자연적인 순서가 있다는 것을 의미한다.

Comparable 을 구현하면 이 인터페이스를 활용하는 수많은 제네릭 알고리즘과 컬렉션의 힘을 누릴 수 있다. 좁쌀만한 노력으로 코끼리만한 효과를 누릴수 있는것이다. 자연적인 순서를 갖는 값 클래스라면 꼭 구현하도록 하자.

다음과 같은 규약을 지켜야 한다.

  1. 역 성립 sgn(x.compareTo(y)) == -sgn(y.compareTo(x))

  2. 추이성 성립 x.compareTo(y) > 0 && y.compareTo(z) -> x.compareTo(z) > 0

  3. x.compareTo(y) == 0 && y.compareTo(z) == 0 -> x.compareTo(z) == 0

  4. x.compareTo(y) == 0 -> x.equals(y) = true

3번은 필수는 아니지만 만약 Comparable을 구현하는데 이게 성립되지 않을 경우 꼭 클래스의 순서는 equals 메서드와 일치하지 않는다 를 명시해야한다.

마지막 규약은 되도록 지키는게 좋은게 정렬된 컬렉션들은 equals 메서드의 규약을 따른다고 되어 있지만 실제로는 동치성을 비교할 때 equals 대신 compareTo 를 사용하는 경우가 많다.

TreeMap.java

if (cpr != null) {
  do {
    parent = t;
    cmp = cpr.compare(key, t.key);
    if (cmp < 0)
    t = t.left;
    else if (cmp > 0)
    t = t.right;
    else
    return t.setValue(value);
  } while (t != null);
}

Collections.java

int indexedBinarySearch(List<? extends Comparable<? super T>> list, T key) {
  int low = 0;
  int high = list.size()-1;

  while (low <= high) {
    int mid = (low + high) >>> 1;
    Comparable<? super T> midVal = list.get(mid);
    int cmp = midVal.compareTo(key);

    if (cmp < 0)
    low = mid + 1;
    else if (cmp > 0)
    high = mid - 1;
    else
    return mid; // key found
  }
  return -(low + 1);  // key not found
}

구현은 다음처럼 자바에서 제공하는 비교자나 직접 비교자를 구현하면 된다.

public final class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
  public int compareTo(CaseInsensitiveString cis) {
    return String.CASE_INSENSITIVE_ORDER.comapre(s, cis.s);
  }
}
public int compareTo(PhoneNumber pn) {
  int result = Short.compare(areaCode, pn.areaCode);
  if (result == 0) {
    result = Short.comapre(prefix, pn.prefix);
    if (result == 0) {
      result = Short.comapre(lineNum, pn.lineNum)
    }
  }
  return result;
}

아니면 함수형 인터페이스 Comparator 를 구현하면 좀 더 선언적으로 가능

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

관계 연산자를 (> , <, ==) 를 직접 쓰는것보다 (거추장스럽고 오류를 유발한다. 맨날 헷갈림) 될 수 있으면 박싱 클래스의 compare 정적 메서드를 활용해라.

비교자를 구현할 때는 성능을 위해 핵심적인 요소부터 비교할것

값의 차 (-) 를 기준으로 Comparator 를 구현하지 말것. 정수 오버플로우와 부동소수점 계산 바식에 따른 오류를 낼 수 있다.

다음 두가지 방법을 써라

static Comparator<Object> hashCodeOrder = new Comparator<>() {
  public int compare(Object o1, Object o2) {
    return Integer.compare(o1.hashCode(), o2.hashCode());
  }
};

static Comparator<Object> hashCodeOrder = Comparator.comparingInt(o -> o.hashCode());
결론

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

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

클래스와 인터페이스

ITEM 15 클래스와 멤버의 접근 권한을 최소화하라

내부 데이터와 내부 구현은 외부로부터 완벽히 숨겨 구현과 API 를 분리하고, 외부에서는 이 API 를 통해서만 소통해야한다.

public 클래스의 인스턴스 필드는 되도록 public 이 아니여야 한다.

private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES =
  Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values() {
  return PRIVATE_VALUES.clone();
};

Java9 모듈?

결론

프로그램 요소의 접근성은 가능한 최소한으로 하라. 꼭 필요한 것만 골라 최소한의 public API 를 설계하자.

그 외에는 클래스, 인터페이스, 멤버가 의도치 않게 API로 공개되는 일이 없도록 해야한다.

public 클래스는 상수용 public static final 필드 외에는 어떠한 public 필드도 가져서는 안 된다. public static final 필드가 참조하는 객체가 불변인지 확인하라.

ITEM 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라

접근자를 제공하고 필드는 private 로 선언할것

결론

public 클래스는 절대 가변 필드를 직접 노출해서는 안 된다.

불변 필드라면 노출해도 덜 위험하지만 완전히 안심할 수는 없다.

하지만 package-private 클래스나 private 중첩 클래스에서는 종종 필드를 노출하는 편이 나을 때도 있다.

ITEM 17. 변경 가능성을 최소화하라

불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.

불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요 없다. 그러므로 안심하고 공유할 수 있고, 최대한 재활용 하기를 권한다.

방어적 복사도 필요없다. 아무리 복사해봐야 원본과 똑같으니 복사 자체가 의미가 없다. 불변 객체는 자유롭게 공유할 수 있음은 물론, 불변 객체끼리는 내부 데이터를 공유할 수 있다. 불변 객체는 그 자체로 실패 원자성을 제공한다.

메서드에서 예외가 발생한 후에도 그 객체는 여전히 유효한 상태여야 한다..

단점 -> 값이 다르면 반드시 독립된 객체로 만들어야 한다.

BigInteger.java

// Compute the modular inverse
int inv = -MutableBigInteger.inverseMod32(mod[modLen-1]);

// Convert base to Montgomery form
int[] a = leftShift(base, base.length, modLen << 5);

MutableBigInteger q = new MutableBigInteger(),
a2 = new MutableBigInteger(a),
b2 = new MutableBigInteger(mod);

MutableBigInteger r= a2.divide(b2, q);
table[0] = r.toIntArray();

// Pad table[0] with leading zeros so its length is at least modLen
if (table[0].length < modLen) {
  int offset = modLen - table[0].length;
  int[] t2 = new int[modLen];
  for (int i=0; i < table[0].length; i++)
  t2[i+offset] = table[0][i];
  table[0] = t2;
}

// Set b to the square of the base
int[] b = squareToLen(table[0], modLen, null);
b = montReduce(b, mod, modLen, inv);
// 생략
return new BigInteger(1, t2);

생성

자신을 상속하지 못하게 하는 가장 쉬운 방법은 final 클래스로 선언하는 것이지만, 모든 생성자를 private 나 package-private 로 두고 public 정적 팩터리 메서드를 제공하는 방법이 더 유연하다.

BigInteger 와 BigDecimal 을 설계할땐 불변 객체가 사실상 final 이어야 하는 인식이 널리 퍼지지 않았다.

그래서 이 두 클래스 모두 재정의할 수 있게 설계되었고, 안타깝게도 하위호환성이 발목을 잡아 지금까지도 이 문제를 고치지 못했다. 만약 신뢰할 수 없는 클라이언트로 부터 이 두 클래스의 인스턴스를 인수로 받는다면 주의해야한다.

신뢰할 수 없는 하위클래스라고 생각된다면 가변이라 가정하고 방어적으로 복사해서 사용해야한다.

결론

게터가 있다고 무조건 세터를 만들지 마라. 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.

String과 BigInteger 처럼 무거운 값 객체도 불변으로 만들 수 있는지 고심해야 한다. 어쩔 수 없다면 불변 클래스와 쌍을 이루는 가변 동반 클래스를 제공해라

불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.

생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야한다.