Effective Java 제5조: 불필요한 객체 만들지 않기

5819 단어
일반적으로 필요할 때마다 같은 기능을 가진 새로운 대상을 만드는 것이 아니라 대상을 다시 사용하는 것이 좋다.중용 방식은 더욱 빠를 뿐만 아니라 더욱 유행한다.만약 대상이 변할 수 없는 (immutable) (15조 참조) 라면, 그것은 시종 다시 사용될 수 있다.
극단적인 반대의 예로 다음 문장을 고려해 보자.
String s = new String("stringette"); // DON'T DO THIS!

이 문장은 실행될 때마다 새 String 실례를 만들지만, 대상을 만드는 동작은 모두 필요하지 않습니다.String 구조자에게 전달되는 매개 변수("stringette")는 그 자체가 String 실례이며, 기능은 구조기가 만든 모든 대상과 같다.만약 이러한 사용법이 하나의 순환에 있거나, 빈번하게 호출되는 방법에 있다면, 수천 수만의 불필요한 String 실례를 만들 것이다.
개선된 버전은 다음과 같습니다.
String s = "stringette";

이 버전은 실행할 때마다 새로운 실례를 만드는 것이 아니라 하나의 String 실례만 사용합니다.그리고 같은 가상 기기에서 실행되는 모든 코드에 대해 같은 문자열의 상수만 포함하면 그 대상은 [JLS, 3.10.5]를 다시 사용할 수 있음을 보장할 수 있다.
정적 공장 방법(제1조 참조)과 구조기의 불변류를 동시에 제공한 경우 구조기가 아닌 정적 공장 방법을 사용하여 불필요한 대상을 만들 수 있다.예를 들어, 정적 플랜트 방법Boolean.value Of (String) 는 거의 항상 구조자 Boolean (String) 보다 우선합니다.구조기는 매번 호출될 때마다 새로운 대상을 만들고 정적 공장 방법은 이렇게 하라고 요구하지 않으며 실제로는 이렇게 하지 않는다.
변경할 수 없는 대상을 다시 사용하는 것 외에 수정되지 않을 것으로 알려진 대상을 다시 사용할 수도 있다.다음은 비교적 미묘하고 흔히 볼 수 있는 반례이다. 그 중에서 가변적인Date 대상과 관련된 것이기 때문에 그들의 값은 일단 계산된 후에 다시는 변하지 않는다.이 유형은 하나의 모델을 구축했다. 그 중 한 사람이 있고 이 사람이'베이비부머(babyboomer)'인지 아닌지를 검사하는 방법이 있다. 다시 말하면 이 사람이 1946년부터 1964년 사이에 태어났는지 검사하는 것이다.
public class Person {
private final Date birthDate;
// Other fields, methods, and constructor omitted
// DON'T DO THIS!
public boolean isBabyBoomer() {
// Unnecessary allocation of expensive object
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&
birthDate.compareTo(boomEnd) < 0;
}
}

isBabyBoomer는 호출될 때마다 Calendar 하나, TimeZone 하나, Date 실례 두 개를 새로 만듭니다. 이것은 필요없습니다.다음 릴리즈에서는 비효율성을 방지하기 위해 정적 초기화기(initializer)를 사용합니다.
class Person {
private final Date birthDate;
// Other fields, methods, and constructor omitted
/**
* The starting and ending dates of the baby boom.
*/
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0 &&
birthDate.compareTo(BOOM_END) < 0;
}
}

개선된 Person 클래스는 초기화할 때마다Calendar, TimeZone, Date 실례를 한 번만 만듭니다. isBabyBoomer가 호출될 때마다 이 실례를 만듭니다.isBabyBoomer 메서드가 자주 호출되면 성능이 크게 향상됩니다.내 기계에서, 매번 천만 번 호출할 때마다, 원래의 버전은 32000ms가 필요하지만, 개선된 버전은 130ms로 약 250배가 빠르다.성능을 향상시키는 것 외에 코드의 의미도 더욱 명확해졌다.boomStart와 boomEnd를 국부 변수에서final 정적 영역으로 바꾸면, 이 날짜들은 분명히 상수로 처리되어 코드를 이해하기 쉽다.그러나 이러한 최적화가 가져오는 효과는 항상 그렇게 뚜렷하지 않다. 왜냐하면 Calendar 실례의 창설 대가가 매우 비싸기 때문이다.
개선된 Person 클래스가 초기화되었지만 isBabyBoomer 방법이 영원히 호출되지 않는다면 BOOM 을 초기화할 필요가 없습니다START 및 BOOMEND 도메인isBabyBoomer 방법이 처음 호출되었을 때 초기화 지연 (lazily initializing) (71조 참조) 을 통해 이러한 불필요한 초기화 작업을 제거할 수 있지만, 이렇게 하는 것을 권장하지 않습니다.지연 초기화(lazy initialization)에서 흔히 볼 수 있는 것처럼 이렇게 하면 방법의 실현을 더욱 복잡하게 하고 이미 달성한 수준을 초과할 수 없다(제55조 참조).
본 항목 앞의 예에서 논의된 대상은 분명히 모두 중용될 수 있다. 왜냐하면 초기화된 후에 다시 바뀌지 않기 때문이다.다른 몇몇 상황들은 결코 항상 이렇게 뚜렷하지 않다.어댑터(adapter)의 상황을 고려하여 [Gamma95,p.139], 때로는 보기(view)라고도 부른다.어댑터는 이러한 대상을 가리킨다. 기능을 백업 대상 (backing object) 에 위탁하여 백업 대상에게 대체할 수 있는 인터페이스를 제공한다.어댑터는 예비 대상을 제외하고는 다른 상태 정보가 없기 때문에 특정한 대상의 특정 어댑터에 대해 여러 개의 어댑터 실례를 만들 필요가 없다.
예를 들어 맵 인터페이스의 키 세트 방법은 맵 대상의 세트 보기를 되돌려줍니다. 이 맵의 모든 키 (키) 를 포함합니다.키셋을 호출할 때마다 새 키셋을 만들어야 하는 것처럼 보이지만, 주어진 맵 대상에 대해 키셋을 호출할 때마다 같은 키셋을 되돌려줍니다.되돌아오는 Set 실례는 일반적으로 바뀔 수 있지만 모든 되돌아오는 대상은 기능적으로 동일하다. 그 중의 되돌아오는 대상이 변화할 때 모든 다른 되돌아오는 대상도 변화가 발생한다. 왜냐하면 그들은 같은 맵 실례가 지탱하기 때문이다.키셋 보기 대상을 만드는 여러 사례는 해가 되지 않지만 필요없습니다.
자바 1.5 릴리스에는 자동 포장(autoboxing)이라는 여분의 대상을 만드는 새로운 방법이 있는데, 프로그래머가 기본 유형과 포장 기본 유형(Boxed Primitive Type)을 혼합하여 필요에 따라 자동으로 포장하고 뜯을 수 있도록 한다.자동 포장은 기본 유형과 포장 기본 유형 간의 차이를 모호하게 만들었지만 완전히 사라지지 않았다.그것들은 의미에서도 미묘한 차이가 있고 성능에서도 비교적 뚜렷한 차이가 있다(제49조 참조).다음 프로그램을 고려하십시오. 모든 int 값의 총계를 계산합니다.이를 위해 프로그램은 long 알고리즘을 사용해야 합니다. int가 크지 않아서 모든 int 값의 총계를 수용할 수 없습니다.
// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}

이 프로그램이 계산한 답안은 정확하지만 실제 상황보다 좀 더 느리다. 단지 한 글자를 잘못 쳤기 때문이다.변수 sum이 long 대신 Long으로 선언됨은 프로그램이 약 231개의 여분의 Long 인스턴스를 구성했음을 의미합니다(Long sum에 long을 추가할 때마다 인스턴스를 구성함).sum의 성명을 롱에서 롱으로 바꾸어 내 기계에서 운행 시간을 43초에서 6.8초로 줄였다.결론은 명백하다. 컨테이너의 기본 유형이 아니라 기본 유형을 우선적으로 사용하고 무의식적인 자동 컨테이너를 조심해야 한다.
본 항목에 소개된 내용이'대상을 만드는 대가가 매우 비싸다고 착각하지 말고 우리는 가능한 한 대상을 만드는 것을 피해야 한다'는 것을 암시해야 한다.반면 작은 대상의 구조기는 아주 적은 양의 현식 작업만 하기 때문에 작은 대상의 창설과 회수 동작은 매우 저렴하다. 특히 현대의 JVM 실현에 있어 더욱 그렇다.추가 대상을 만들어 프로그램의 명확성과 간결성, 기능성을 향상시키는 것은 보통 좋은 일이다.
반대로 자신의 대상 탱크(object pool)를 유지함으로써 대상을 만드는 것을 피하는 것은 좋은 방법이 아니다. 탱크의 대상이 매우 무거운 것이 아니라면.진정으로 대상 탱크를 정확하게 사용하는 전형적인 대상 예는 데이터베이스 연결 탱크이다.데이터베이스 연결을 구축하는 대가가 매우 비싸기 때문에 이런 대상을 다시 사용하는 것은 매우 의미가 있다.그리고 데이터베이스 허가가 일정 수량의 연결만 사용할 수 있도록 제한할 수도 있다.그러나 일반적으로 자신의 대상 탱크를 유지하는 것은 코드를 어지럽히고 메모리 점용(footprint)을 증가하며 성능을 손상시킬 수 있다.현대의 JVM은 고도로 최적화된 스팸 수거기를 실현하는데 그 성능은 경량급 대상 탱크의 성능을 초과하기 쉽다.
이 항목에 해당하는 내용은 제39조의'보호 복제(defensive copying)'에 관한 내용이다.이 항목은'기존 대상을 다시 사용해야 할 때 새로운 대상을 만들지 마세요'를 언급하고, 39조는'새로운 대상을 만들어야 할 때 기존 대상을 다시 사용하지 마세요'라고 말한다.보호 복사본 사용을 권장할 때 중복 대상을 만드는 것보다 중복 대상을 만드는 대가가 훨씬 크다는 것을 주의해야 한다.필요할 때 보호 복사를 실시하지 못하면 잠재적인 오류와 안전 빈틈을 초래할 수 있다.대상을 불필요하게 만드는 것은 프로그램의 스타일과 성능에만 영향을 미친다.

좋은 웹페이지 즐겨찾기