2장 객체의 생성과 삭제
규칙 1. 생성자 대신 정적 팩터리 메서드를 사용할 수 없는지 생각해 보라
장점
- 생성자와는 달리 정적 팩터리 메서드에는 이름(name)이 있다.
- 생성자와는 달리 호출할 때마다 새로운 객체를 생성할 필요는 없다.
- 생성자와는 달리 반환값 자료형의 하위 자료형 객체를 반환할 수 있다.
- 형인자 자료형(parameterized type) 객체를 만들 때 편하다.
단점
- public이나 protected로 선언된 생성자가 없으므로 하위 클래스를 만들 수 없다.
- 정적 팩터리 메서드가 다른 정적 메서드와 확연히 구분되지 않는다.
규칙 2. 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라
- 점층적 생성자 패턴은 잘 동작하지만 인자 수가 늘어나면 클라이언트 코드를 작성하기 어려워지고, 무엇보다 읽기 어려운 코드가 되고 만다.
- 자바빈 패턴은 1회의 함수 호출로 객체 생성을 끝낼 수 없으므로, 객체 일관성(consistency)이 일시적으로 깨질 수 있다.
- 자바빈 패턴으로는 변경 불가능(immutable) 클래스를 만들 수 없다.
- 빌더 패턴은 인자가 많은 생성자나 정적 팩터리가 필요한 클래스를 설계할 때, 측히 대부분의 인자가 선택적 인자인 상황에 유용하다.
규칙 3. private 생성자나 enum 자료형은 싱글턴 패턴을 따르도록 설계하라
규칙 4. 객체 생성을 막을 때는 private 생성자를 사용하라
- 정적 메서드나 필드만 모은 유틸리티 클래스와 같은 객체를 만들 목적의 클래스가 아니다.
- 생성자를 생략하면 컴파일러는 자동으로 인자없는 public 기본 생성자를 만들어 버린다.
- private 생성자를 클래스에 넣어서 객체 생성을 방지하자.
private UtilityClass() {
throw new AssertionError();
...
}
규칙 5. 불필요한 객체는 만들지 말라
변경 불가능 클래스
- 기능적으로 동일한 객체는 필요할 때마다 만드는 것보다 재사용하는 편이 낫다.
- 변경 불가능(immutable) 객체는 언제나 재사용할 수 있다. (규칙 15)
String s = new String("stringette"); // 이러면 곤란하다!
String s = "stringette"; // Good!
- 생성자와 정적 팩터리 메서드(규칙 1)를 함께 제공하는 변경 불가능 클래스의 경우, 생성자 대신 정적 팩터리 메서드를 이용하면 불필요한 객체 생성을 피할 수 있다.
정적 초기화 블록 사용
public boolean isBabyBoomer() {
// 생상 비용이 높은 객체를 쓸데없이 생성한다.
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calenader.JANUARY, 1, 0, 0, 0);
DATE boomStart = gmtCal.getTime();
gmtCal.set(1965, Calenader.JANUARY, 1, 0, 0, 0);
DATE boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&
birthDate.compareTo(boomEnd) < 0;
}
- 위 예제는 메서드가 호출될 때마다 Calendar, TimeZone, Date 객체를 쓸데없이 만들어 낸다.
- 이렇게 비효율적인 코드는
정적 초기화 블록(static initializer)
을 통해 개선하는 것이 좋다.
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
alendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calenader.JANUARY, 1, 0, 0, 0);
DATE boomStart = gmtCal.getTime();
gmtCal.set(1965, Calenader.JANUARY, 1, 0, 0, 0);
DATE boomEnd = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(boomStart) >= 0 &&
birthDate.compareTo(boomEnd) < 0;
}
- 코드 또한 간결하고 명료해졌다.
- boomStart, boomEnd를 static final 필드로 바꾸자 이 두 날짜가 상수라는 사실이 분명하게 드러나 읽기 좋은 코드가 되었다.
자동 객체화
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
- sum은 long이 아니라 Long으로 선언되어 있어 2^31 개의 쓸데없는 개체가 만들어 진다.
- 객체 표현형 대신 기본 자료형을 사용하고, 생각지도 못한 자동 객체화가 발생하지 않도록 유의하자.
규칙 6. 유효기간이 지난 객체 참조는 폐기하라
public class Stack {
...
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
}
이 프로그램은 메모리 누스(memory leak) 문제가 있다.
메모리 누수는 어디에서 생기는가? 스택이 커졌다가 줄어들면서 제거한 객체들을 쓰레기 수집기가 처리하지 못해서 생긴다. 스택이 그런 객체에 대한 만기 참고(obsolete reference)를 제거하지 않기 때문이다. 만기 참조란, 다시 이용되지 않을 참조(reference)를 말한다.
이런 문제는 간단히 고칠 수 있다. 쓸일 없는 객체 참조는 무조건 null로 만드는 것이다.
public class Stack {
...
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object results = elements[--size];
elements[size] = null; // 만기 참조 제거
return results;
}
}
자체적으로 관리하는 메모리가 있는 클래스를 만들 때는 메모리 누수가 발생하지 않도록 주의해야 한다.
- 더 이상 사용되지 않는 원소 안에 있는 객체 참조는 반드시 null로 바꿔 주어야 한다.
캐시(cache)도 메모리 누수가 흔히 발생하는 장소이다.
- WeakHashMap을 가지고 캐시를 구현한다. 캐시 바깥에서 키를 참조하고 있을 때만 값을 보관하면 될 때 쓸 수 있는 전략이다.
메모리 누수가 흔히 발견되는 또 한 곳은 리스너(listener) 등의 역호출자(callback)이다.
- 쓰레기 수집기가 역호출자를 즉시 처리하도록 할 가장 좋은 방법은 역호출자에 대한 약한 참조(weak reference)만 저장하는 것이다.
규칙 7. 종료자 사용을 피하라
종료자(finalize)는 예측 불가능하며, 대체로 위험하고, 일반적으로 불필요하다.
종료자 사용은 피하는 것이 원칙
이다.
C++에서 소멸자는 객체에 배정된 자원을 반환하는 일반적인 수단이며, 생성자와 쌍으로 존재해야 한다. 자바에서는 더 이상 참조되지 않는 객체에 할당된 공간을 쓰레기 수집기가 알아서 반환하므로 프로그래머 입장에서 특별히 할 일이 없다.
어떤 객체에 대한 모든 참조가 사라지고 나서 종료자가 실행되기까지는 긴 시간이 걸릴 수도 있다.
긴급한 작업을 종료자 안에서 처리하면 안된다. 예를 들어, 종료자 안에서 파일을 닫도록 하면 치명적이다.
uncaught 예외가 발생하면 스레드는 종료되고 스택 추적 정보(stack trace)가 표시되지만, 종료자 안에서는 아니다.
종료 처리 도중에 무점검(uncaught) 예외가 던져지면, 해당 예외는 무시되며 종료 과정은 중단된다. 일반적으로 uncaught 예외가 발생하면 스레드는 종료되고 스택 추적 정보(stack trace)가 표시되지만, 종료자 안에서는 아니다. 경고 문구 조차 출력되지 않는다.
종료자를 사용하면 프로그램 성능이 심각하게 떨어진다.
명시적인 종료 메서드를 하나 정의하라.
명시적인 종료 메서드의 예로는 OutputStream이나 InputStream, java.sql.Connection에 정의된 close 메서드 이다.
이런 명시적인 종료 메서드는 보통 try-finally 문과 함께 쓰인다. 객체 종료를 보장하기 위해서다.
Foo foo = new Foo(...);
try {
//foo로 해야하는 작업 수행
...
} finally {
foo.terminate(); //명시적 종료 메서드 호출
}
종료자 연결(finalizer chaining)이 자동으로 이루어지지 않는다.
만일 어떤 클래스가 종료자를 갖고 있고 하위 클래스가 해당 메서드를 재정의하는 경우, 하위 클래스의 종료자는 상위 클래스 종료자를 명시적으로 호출해야 한다. 이때, 하위 클래스의 상태는 try 블록 안에서 종료시켜야 하고, 상위 클래스 종료자는 finally 블록 안에서 호출해야 한다.
@Override
protected void finalize() throws Throwable {
try {
... // 하위 클래스의 상태를 종료함
} finally {
super.finalize();
}
}
종료 보호자 패턴
하위 클래스에서 상위 클래스 종료자를 재정의 하면서 사위 클래스 종료자 호출을 잊으면, 상위 클래스 종료자는 절대로 호출되지 않는다.
이런 문제를 방지하는 방법은 모든 객체마다 여벌의 객체를 하나 더 만드는 것이다.
종료되어야 하는 개체의 클래스 안에 종료자를 정의하는 대신, 익명 클래스(anonymous class)안에서 종료자를 정의하는 것이다.
이 익명 클래스로 만든 객체는 종료 보호자(finalize guardian)
라고 부른다.
// 종료 보호자 숙어(Finalize Guardian idiom)
public class Foo {
// 이 객체는 바깥 객체(Foo 객체)를 종료시키는 역할만 한다.
private final Object finalizerGuardian = new Object() {
@Override
protected void finalize() throws Throwable {
... // 바깥 Foo 객체를 종료 시킴
}
}
...
}
Notice: 이 글은 [Effective Java 2/E]를 스터디하면서 정리한 내용입니다.