똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을때가 많다.(String 객체 생성 방식과 리터럴 방식의 차이점)
- 재사용은 빠르고 세련되며 특히 불변 객체는 언제든 재사용할 수 있다.
- 다음 코드는 하지 말아야할 극단적 예이니 유심히 살펴보자.
1
String s = new String("bikini");
- 위 코드는 실행될 때마다 String 인스턴스를 새로 만든다. 완전히 쓸데 없는 행위다. 10000번이 호출된다면 10000개가 생성될 것이다.
- 개선된 버전은 아래와 같다.
1
String s = "bikini";
- 위 코드는 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용한다.
- 위 방식처럼 사용한다면 하나의 JVM 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.
생성자 대신 정적 팩터리 메서드를 제공하는 불변 클래스에서는 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다.
- Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메서드를 사용하는 것이 좋다.
- 그래서 이 생성자는 자바9에서 사용 자제(deprecated) API로 지정되었다.
- Boolean 래핑 클래스를 보면 아래와 같이 미리
TRUE
,FALSE
값을 나타내는 Boolean객체를 생성해서 가지고 있다.
그래서 valueOf를 호출하면 미리 생성되어 있는 Boolean 객체를 반환하게 된다.
- 생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않다.
- 불변 객체만이 아니라 가변 객체라 해도 사용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.
- 생성 비용이 비싼 객체가 반복해서 필요한 경우엔 캐싱해서 재사용하길 권한다.
String.matches 한 번 주의 깊게 살펴보자.
1
2
3
4
5
// 코드 6-1 성능을 훨씬 더 끌어올릴 수 있다!
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
- String.matches는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다.
matches()
메서드 내부에서 만드는 정규표현식용 Pattern 인스턴스는 한 번 쓰고 버려져서 곧바로 GC의 대상이 된다.- 성능을 개선하려면 필요한 정규표현식을 표현하는 (불변인) Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 나중에
isRomanNumeral
메서드가 호출될 때마다 이 인스턴스를 재사용한다.
1
2
3
4
5
6
7
8
// 코드 6-2 값비싼 객체를 재사용해 성능을 개선한다.
private static final Pattern ROMAN = Pattern.compile(
"^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeralFast(String s) {
return ROMAN.matcher(s).matches();
}
- 이렇게 개선하면 isRomanNumeral 이 빈번히 호출되는 사오항에서 성능을 상당히 끌어올릴 수 이;ㅆㄷ다.
- 저자의 PC로 테스트했을 당시 6.5배 정도 속도가 빨라진 것을 확인할 수 있다고 한다.
- 성능만 좋아진 것이 아니라 코드도 훨씬 명확해졌다.
- Pattern 인스턴스를 static final 필드로 끄집어내고 이름을 지어주어 코드의 의미가 훨씬 잘 드러난다.
만약 isRomanNumeral 메서드가 한 번도 호출하지 않는다면 지연 초기화를 통해 불필요한 초기화를 없앨 수 있지만, 권하진 않는다.
- 해당 메서드가 한 번도 호출되지 않는다면 불필요한 초기화를 한 게 될 것이다. 이를 개선하기 위해 지연초기화를 생각해볼 수 있다.
지연초기화란?
필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법이다.- 하지만 지연 초기화는 코드를 복잡하게 만드는데, 성능은 크게 개선되지 않을때가 많다.(아이템67)
객체가 불변이라면 재사용해도 안전함이 명백하다.
- 어댑터는 실제 작업은 뒷단 객체에 위임하고, 자신은 제 2의 인터페이스 역할을 해주는 객체다.
- 그러기에 어댑터는 뒷단 객체만 관리해주면 된다.
불필요한 개체를 만들어내는 또 다른 예로 오토박싱(auto boxing)을 들 수 있다.
- 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.
- 의미상으론 별다를게 없지만 성능상으로 그렇지 않다.(아이템61)
1
2
3
4
5
6
7
8
9
// 코드 6-3 끔찍이 느리다! 객체가 만들어지는 위치를 찾았는가? (34쪽)
public class Sum {
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
}
- 위의 메서드는 sum 변수를 long이 아닌 이를 래핑한 Long 으로 선언해서 불필요한 Long 인스턴스가 약 2의 31제곱개나 만들어진 것이다.
- 단순히 sum의 타입을 long으로만 바꿔주면 6.3초에서 0.59초로 빨라진다.
Note: 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
이번 아이템을 “객체 생성은 비싸니 피해야 한다” 로 오해하면 안된다.
- 요즘의 JVM에선 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담되진 않는다.
- 프로그램의 간결성, 명확성, 기능을 위해선 객체를 추가로 생성하는 것은 일반적은 좋은 예이다.
- 거꾸로 아주 문거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 객체 풀(pool)을 만들진 말자. 물론 객체 풀을 만드는게 더 나은 예가 있긴하다. DB 커넥션 같은 경우엔 생성 비용이 워낙 비싸니 재사용하는 편이 낫다.
방어적 복사(defensive copy)가 필요한 상황에서 객체를 재사용했을 때의 피해가, 필요 없는 객체를 반복생성했을 때의 피해보다 훨씬 크다는 사실을 기억하자.
- 방어적 복사에 실패하면 언제 터져 나올지 모르는 버그와 보안구멍으로 이어지지만, 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 준다.