Cloneable 인터페이스는 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스이다.
하지만 clone() 메서드는 Cloneable 인터페이스가 아닌 Object 클래스에 선언이 되어있다.
메서드가 하나도 선언되어 있지 않은 Cloneable 인터페이스는 Object의 protected 메서드인 clone() 메서드의 동작 방식을 결정힌다.
Cloneable을 구현한 클래스의 인스턴스에서 clone()을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, Cloneable을 구현하지 않은 클래스에서 이를 호출하면 CloneNotSupportedException을 던진다.
clone() 메소드는 클래스에 정의된 모든 필드가 기본 타입이거나 불변 객체를 참조하는 경우에 완벽하게 맞는다.
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 X
}
편안하네요.
@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()의 결과를 그대로 반환한다면 반환된 인스턴스의 size 필드는 올바른 값을 갖지만, elements 필드는 원본 Stack 인스턴스와 같은 배열을 참조한다.
따라서 원본이나 복제본 인스턴스 중 하나를 수정하면 다른 하나도 같이 수정되어 불변식을 해치게 된다.
배열의 clone()은 런타임 타입과 컴파일타임 타입 모두가 원본 배열과 똑같은 배열을 반환하므로 형변환이 필요없다.
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final 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 AssertionError();
}
}
재귀적인 호출로 인해 연결 리스트의 원소 수만큼 스택 프레임을 소비하기 때문에 연결 리스트가 길다면 StackOverflow를 일으킬 위험이 있다.
Entry deepCopy() {
Entry result = new Entiry(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;
}
deepCopy()를 재귀적으로 호출하는 대신 반복자를 써서 순회하는 방향으로 수정했다.
Comparable의 compareTo() 메소드는 단순 동치성 비교뿐만 아니라 순서까지 비교할 수 있으며 제네릭하다.
마지막 규약은 필수는 아니지만 꼭 지키는 것이 좋다.
public class CaseInsensitiveString implements Comparable<CaseInsensitiveString> {
public int compareTo(CaseInsensitiveString cis) {
return String.CASE_INSENSITIVE_ORDER.compare(s, cis.s);
}
// 나머지 코드 생략
}
public int compareTo(Number n) {
int result = Integer.compare(first, n.first); // 가장 중요한 필드
if (result == 0) {
result = Integer.compare(second, n.second); // 두 번째로 중요한 필드
if (result == 0) {
result = Integer.compare(third, n.third); // 세 번째로 중요한 필드
}
}
return result;
}
접근 제한자의 기본 원칙은 모든 클래스와 멤버의 접근성을 가능한 한 좁혀야한다.
-> 클래스의 공개 API를 제외한 모든 멤버는 private로 만든다.
-> 그 후 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한하여 package-private로 풀어주자.
class Point {
public double x;
public double y;
}
class Point {
private double x;
private double y;
public Point2(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public void setX(double x) {
this.x = x;
}
public void setY(double y) {
this.y = y;
}
}
public final class Complex {
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public double realPart() {
return re;
}
public double imaginaryPart() {
return im;
}
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
public Complex minus(Complex c) {
return new Complex(re * c.re, im - c.im);
}
public Complex times(Complex c) {
return new Complex(re * c.re - im * c.im,
re * c.im + im * c.re);
}
public Complex dividedBy(Complex c) {
double tmp = c.re * c.re + c.im * c.im;
return new Complex((re * c.re + im * c.im) / tmp,
(im * c.re - re * c.im) / tmp);
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Complex))
return false;
Complex c = (Complex) o;
return Double.compare(c.re, re) == 0
&& Double.compare(c.im, im) == 0;
}
@Override
public int hashCode() {
return 31 * Double.hashCode(re) + Double.hashCode(im);
}
@Override
public String toString() {
return "(" + re + " + " + im + "i)";
}
}
값이 다르면 반드시 독립된 객체로 만들어야 한다.
-> 모든 생성자를 private 혹은 package-private으로 만들고 public 정적 팩토리를 제공한다.
public class Complex {
private final double re;
private final double im;
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
//나머지 코드 생략
}
클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소한으로 줄이자.
다른 합당한 이유가 없다면 모든 필드는 private final이어야 한다.
상속은 코드를 재사용하는 강력한 수단이지만 다음과 같은 문제가 있다.
public class InstrumentedHashSet<E> extends HashSet<E> {
//추가된 원소의 수
private int addCount = 0;
public InstrumentedHashSet() {}
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
//접근자 메소드
public int getAddCount() {
return addCount;
}
}
기존의 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 한다.
-> 컴포지션(Composition)
새 클래스의 인스턴스 메서드들은 private 필드로 참조하는 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다.
-> 전달(forwarding)
// 래퍼 클래스
public class InstrumentedSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedSet(Set<E> s) {
super(s);
}
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
// 전달 클래스
public class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
public ForwardingSet(Set<E> s) {
this.s = s;
}
@Override
public void clear() { s.clear(); }
@Override
public boolean contains(Object o) { return s.contains(o); }
@Override
public boolean isEmpty() { return s.isEmpty(); }
@Override
public int size() { return s.size(); }
@Override
public Iterator<E> iterator() { return s.iterator(); }
@Override
public boolean add(E e) { return s.add(e); }
@Override
public boolean remove(Object o) { return s.remove(o); }
@Override
public boolean containsAll(Collection<?> c) { return s.containsAll(c); }
@Override
public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
@Override
public boolean removeAll(Collection<?> c) { return s.removeAll(c); }
@Override
public boolean retainAll(Collection<?> c) { return s.retainAll(c); }
@Override
public Object[] toArray() { return s.toArray(); }
@Override
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@Override
public int hashCode() { return s.hashCode(); }
@Override
public boolean equals(Object obj) { return s.equals(obj); }
@Override
public String toString() { return s.toString(); }
}