[이펙티브자바] 아이템 13, 14, 15 요약 정리
아이템 13. clone 재정의는 주의해서 진행하라
Cloneable : 복제해도 되는 클래스임을 명시하는 용도의 인터페이스
-> 의도한 목적을 제대로 이루지 못함.
가장 큰 문제 : clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고, protected 접근제어자
그래서 Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.
Cloneable이 하는 일
- Cloneable 인터페이스를 구현하지 않고 Object's clone 메서드 사용시 'CloneNotSupportedException' 발생
- Object의 protected 메서드인 clone의 동작 방식을 결정
- Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체 반환
Clone 메서드가 제대로 동작하려면
상위 클래스가 제대로 동작하는 clone 메서드를 가지고, Cloneable interface를 구현(implements) 해줄 때 제대로 동작한다.
가변 상태를 참조하지 않는 clone 메서드
super.clone을 호출한다. => 얻은 객체는 원본의 완벽한 복제본이다. (클래스에 정의된 모든 필드는 원본 필드와 똑같은 값을 갖는다.)
public static class PhoneNumber implements Cloneable {
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
- Object의 clone 메서드는 Object를 반환하지만 PhoneNumber의 clone 메서드는 PhoneNumber을 반환하게 한다.
- 재정의한 메서드의 반환 타입은 상위 클래스의 메서드가 반환하는 타입의 하위 타입일 수 있다.
- Object에서는 protected로 선언되어있지만, PhoneNumber 클래스에서는 clone()를 public 제한자로 변경할 수 있다.
가변 상태를 참조하는 clone 메서드
만약 PhoneNumber 클래스에 short[] 배열이 존재한다고 가정해보자.
위의 가변 객체를 참조하는 순간, clone 메서드로 복제시 short[] 배열을 원본 인스턴스와 복제 인스턴스에서 공유해서 같은 객체를 참조하는 문제가 발생한다.
해결하기 위해선 short[] 배열의 clone을 재귀적으로 호출하는 것이다.
public static class PhoneNumber implements Cloneable {
short[] elements;
@Override
public PhoneNumber clone() {
try {
PhoneNumber result = (PhoneNumber) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
elements(상위 객체)의 clone 메서드를 호출함으로써 참조 필드에 대해 복사를 진행한다.
요약 : Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다. 이때 접근 제한자는 public, 반환 타입은 클래스 자신으로 변경한다.
하지만 Cloneable을 구현한 클래스가 아니라면 다른 방법으로 clone보다 더 나은 객체 복사 방식을 제공할 수 있다.
복사 생성자 / 복사 팩터리
- 복사 생성자 : 단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자
- 복사 팩터리 : 복사 생성자를 모방한 정적 팩터리
public static class PhoneNumber {
int[] elements = new int[]{1, 2, 3, 4, 5};
public PhoneNumber(){}
// 복사 생성자
public PhoneNumber(PhoneNumber phoneNumber) {
// phoneNumber 인스턴스의 객체를 복사하는 과정을 작성한다.
for (int i = 0; i < phoneNumber.elements.length; i++) {
elements[i] = phoneNumber.elements[i];
}
}
// 복사 팩터리
public static PhoneNumber newInstance(PhoneNumber phoneNumber) {
PhoneNumber instance = new PhoneNumber();
for (int i = 0; i < phoneNumber.elements.length; i++) {
instance.elements[i] = phoneNumber.elements[i];
}
return instance;
}
}
- 복사 생성자와 복사 팩터리는 해당 클래스가 구현한 '인터페이스' 타입의 인스턴스를 인수로 받을 수 있다.
HashSet<Integer> set = new HashSet<>();
TreeSet<Integer> treeSet = new TreeSet<>(set);
List<Integer> list = new ArrayList<>();
List<Integer> copy = new ArrayList<>(list);
-> 이런 식으로 Collection이나 Map 타입의 변환에 사용된다.
결론 : 복제 기능은 복사 생성자와 팩터리를 이용하자! 단, 배열만은 clone 메서드 방식이 가장 깔끔하다.
아이템 14. Comparable을 구현할지 고려하라
Comparable 인터페이스의 유일무이한 메서드인 compareTo는
- 단순 동치성 비교에 더해 순서까지 비교할 수 있다.
- 제네릭하다.
위 두가지로 성격을 나타낼 수 있다.
알파벳, 숫자, 연대 같이 순서가 명확한 값 클래스를 작성한다면 반드시 Comparable 인터페이스를 구현하자!
compareTo 메서드의 일반 규약
- 이 객체가 주어진 객체보다 작으면 음의 정수를, 같으면 0을, 크면 양의 정수를 반환한다.
- sgn : 부호 함수 (표현식)
- Comparable을 구현한 클래스는 모든 x, y에 대해 sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) 어야 한다.
- Comparable을 구현한 클래스는 추이성을 보장해야 한다.
즉, (x.compareTo(y) > 0 && y.compareTo(z) > 0) 이면 x.compareTo(z) > 0이다. - x.compareTo(y) == 0 이면 sgn(x.compareTo(z)) == sgn(y.compareTo(z)) 다.
- 필수 x : (x.compareTo(y) == 0) == (x.equals(y)) 여야 한다.
compareTo 메서드 작성 과정
- Comparable은 타입을 인수로 받는 제네릭 인터페이스이므로 compareTo 메서드의 인수 타입은 컴파일 타임에 정해진다.
- compareTo 메서드는 각 필드가 동치인지를 비교하는 것이 아닌 그 순서를 비교한다.
- Comparable을 구현하지 않은 필드나 표준이 아닌 순서로 비교한다면 비교자(Comparator)를 사용한다.
- 비교자는 직접 만들거나 자바가 제공하는 것 중에 골라 쓰면 된다.
- compareTo 메서드에서 정수 기본 타입 필드를 비교할 때 기존엔 관계연산자 <, >를, 실수 기본 타입 필드를 비교할 때는 Double.compare와 Float.compare를 사용했지만
자바 7부터는 박싱된 기본 타입 클래스들에 새로 추가된 정적 메서드인 compare를 이용한다. - 가장 핵심적인 필드부터 비교해 나가는 것이 좋다.
public static class PhoneNumber implements Comparable<PhoneNumber> {
short areaCode, prefix, lineNum;
@Override
public int compareTo(PhoneNumber o) {
int result = Short.compare(areaCode, o.areaCode); // 가장 중요한 필드
if (result == 0) {
result = Short.compare(prefix, o.prefix); // 두 번째로 중요한 필드
if (result == 0) {
result = Short.compare(lineNum, o.lineNum); // 세 번째로 중요한 필드
}
}
return result;
}
}
- 아래는 자바8부터 사용 가능한 비교자(Comparator)를 사용한 방식이다.
public static class PhoneNumber {
short areaCode, prefix, lineNum;
private static final Comparator<PhoneNumber> COMPARATOR =
Comparator.comparingInt((PhoneNumber pn) -> pn.areaCode)
.thenComparingInt(pn -> pn.prefix)
.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
return COMPARATOR.compare(this, pn);
}
}
결론
- 순서를 고려해야 하는 값 클래스를 작성한다면 꼭 Comparable 인터페이스를 구현하자.
- 필드의 값을 비교할 때 관계 연산자를 사용하지 말고 정적 compare 메서드나 Comparator 인터페이스가 제공하는 비교자 생성 메서드를 사용하자.
아이템 15. 클래스와 멤버의 접근 권한을 최소화하라
잘 설계된 컴포넌트는 모든 내부 구현을 완벽히 숨겨, 구현과 API를 깜끔히 분리한다.
오직 API를 통해서만 다른 컴포넌트와 소통하며 서로의 내부 동작 방식에는 전혀 신경쓰지 않는다.
위 원리는 정보 은닉, 혹은 캡슐화를 이용해서 숨길 수 있다.
정보은닉 장점(4)
- 시스템 개발 속도를 높인다. (병렬 개발 가능)
- 시스템 관리 비용 낮춘다. (각 컴포넌트를 더 빨리 파악하여 디버깅할 수 있고 다른 컴포넌트로 교체하는 부담도 적기 때문)
- 성능 최적화에 도움이 된다. (성능을 높여주진 않지만 다른 컴포넌트에 영향을 주지 않고 해당 컴포넌트만 최적화할 수 있기 때문)
- 소프트웨어 재사용성 높인다.
- 큰 시스템을 제작하는 난이도를 낮춰준다. (시스템 전체가 미완이더라도 개별 컴포넌트의 동작을 검증할 수 있기 때문)
정보 은닉의 다양한 장치
- 접근 제어 메커니즘 : 클래스, 인터페이스, 멤버의 접근성을 명시
- 이 접근 제한자(private, protected, public)를 제대로 활용하는 것이 정보 은닉의 핵심
정보 은닉의 기본 원칙
모든 클래스와 멤버의 접근성을 가능한 한 좁혀야 한다.
(= 소프트웨어가 올바로 동작하는 한 항상 가장 낮은 접근 수준을 부여해야 한다.)
- 패키지 외부에서 쓸 이유가 없다면 package-private(default) 으로 선언하자.
- 클라이언트에 아무 피해 없이 다음 릴리즈에 수정, 교체, 제거할 수 있다.
- 한 클래스에서만 사용하는 package-private(default) 클래스나 인터페이스는 이를 사용하는 클래스 안에 private static으로 중첩시켜 사용하자.
- public일 필요가 없는 클래스의 접근 수준을 package-private 톱레벨 클래스로 좁히자.
접근 수준 4가지
- private : 멤버를 선언한 톱레벨 클래스(가장 바깥 클래스)에서만 접근할 수 있다.
- package-private(default) : 멤버가 소속된 패키지 안의 모든 클래스에서 접근할 수 있다.
- protected : package-private 의 접근 범위를 포함하며, 이 멤버를 선언한 클래스의 하위 클래스(상속)에서도 접근할 수 있다.
- public : 모든 곳에서 접근할 수 있다.
멤버 접근 권한 주의사항 (고려할 점)
- 클래스의 공개 API를 세심히 설계한 후, 그 외의 모든 멤버는 private으로 만들자.
- 오직 같은 패키지의 다른 클래스가 접근해야 하는 멤버에 한하여 package-private(default)로 풀어주자.
- 권한을 풀어 주는 일을 자주 하게 된다면 컴포넌트를 더 분해해야 하는지 고민해보자.
- private, package-private 멤버는 모두 해당 클래스의 구현에 해당하므로 보통은 공개 API에 영향을 주지 않는다. 단, Serialziable을 구현한 클래스에서는 그 필드들이 의도치 않게 공개 API가 될 수 있다.
- protected 멤버의 수는 적을수록 좋다. (접근 대상 범위가 넓어지기 때문)
- public 클래스의 인스턴스 필드는 되도록 public이 아니어야 한다. (예외. 상수일 경우 public static final 필드로 공개)
- public static final 필드는 불변이어야 한다.
- 상수의 이름은 대문자 알파벳으로 쓰며 단어 사이에 밑줄(_)을 넣는다.
- 상수는 기본 타입 값이나 불변 객체를 참조해야 한다.
- public static final 배열 필드를 두거나 이 필드를 반환하는 접근자 메서드를 제공해서는 안된다.
public static final Thing[] VALUES = { ... };
참조 타입(배열)에 경우 접근자를 제공하면 final로 선언해도 내용을 수정할 수 있게 된다.
해결책
1. private로 만들고 public 불변 리스트를 추가한다.
private static final Thing[] PRIVATE_VALUE = { ... };
public static final List<Thing> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES);
2. 배열을 private로 만들고 그 복사본을 반환하는 public 메서드를 추가하는 방법(방어적 복사)
private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
return PRIVATE_VALUES.clone();
}