똑같은 기능의 객체를 매번 생성하기 보다는 재사용하는 편이 더 좋을 때가 많다.
String s = new String("don't do this");
위의 예시에서는 문장이 실행될 때마다 String 인스턴스를 새로 만든다. 따라서 String s = "do like this";
와 같이 사용하여 하나의 String 인스턴스를 사용하도록 하자. 이는 반복문등에 의해 여러번 문장이 실행될 때 확연한 성능차이를 보일 것이다.
또한, 같은 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.
생성자는 호출할 때마다 새로운 객체를 만들지만, 팩터리 메서드는 전혀 그렇지 않다.
생성 비용이 아주 비싼 객체들은 반복해서 계속 생성하게 되면 성능에 안좋은 영향을 끼치므로 캐싱하여 재사용하는 것을 권한다. 아래의 예제를 살펴보자.
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})$");
}
여기서 matches()
메서드는 정규 표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만,성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다.
matches()
를 들어가보자.
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
위와 같이 Pattern의 matches()
를 사용하고 있는 것을 볼 수 있다. 그렇다면 Pattern의 matches()
는 어떻게 생겼을까?
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
Pattern의 comile()
이란 것을 사용하고 있다. 그렇다면 compile
로 한번만 더 들어가보자.
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}
Pattern 객체를 생성하고 있는 것을 볼 수 있다. 즉, 맨 위에서 살펴본것 처럼 String.matches()
를 여러번 실행할 경우 실행될 때 마다 Pattern이 실행되는 것이다. 따라서 이를 사용할 때는 Pattern을 만들어 놓고 재사용할 수 있도록 코드를 수정해야한다.
public class RomanNumber {
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 isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
위와 같이 코드를 수정하면 Pattern.compile()
을 통해 Pattern을 생성해 ROMAN에 저장해놓고 사용할 수 있다. 처음 한번만 Pattern 객체를 생성하면 되니 반복해서 matches()
를 사용할 때 보다 효율적이다.
또한 코드도 더 명확해졌다. 개선 전에는 존재조차 몰랐던 Pattern 인스턴스를 static final 필드로 꺼내고 이름을 지어주어 코드의 의미가 훨씬 잘 드러난다.
하지만 이 코드도 문제가 있는데, isRomanNumeral 메소드가 호출되지 않는다면, ROMAN을 필요없이 만든셈이 된다. 게으른 초기화(lazily initializing)를 사용해서 최적화 할 수 있지만 추천하진 않는다. 보통 지연 초기화는 측정 가능한 성능 개선 없이 구현을 복잡하게 만든다.
객체가 불변이라면 재사용해도 안전함이 명백하지만, 훨씬 덜 명백하거나 직관에 반대되는 상황도 있다. Map 인터페이스의 KeySet 메서드를 생각해보자.
KeySet을 사용하면 map안에 있는 key들을 담은 Set이 반환되는데, keySet이 호출될 때마다 새로운 Set인스턴스를 반환한다고 생각할 수도 있으나, 매번 같은 Set 인스턴스를 반환할지도 모른다. 즉, 반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀐다.
public class Main {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("test1", 1);
map.put("test2", 2);
Set<String> keySet1 = map.keySet();
Set<String> keySet2 = map.keySet();
keySet1.remove("test1");
System.out.println(keySet1);
System.out.println(keySet2);
}
}
결과는 keySet1에서만 test1이 없어졌을 것 같지만 keySet2에서도 test1이 없어졌다.
//결과
[test2]
[test2]
프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다.
기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.
import java.util.Comparator;
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
으로 선언되어 있지만 sum에 더해지는 i는 long
으로 선언되어 있다. 따라서 i는 sum에 더해지는 연산 즉, sum += i;
이 수행될 때마다 오토 박싱이 일어난다. 따라서 불필요한 Long 인스턴스가 Integer.MAX_VALUE + 1개 만큼 만들어지는 것이다. 하지만 sum의 타입을 long으로 바꿔주면 이러한 오토박싱은 일어나지 않게 되어 성능이 무척 좋다진다.
따라서 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
우리는 이번 아이템을 통해 같은 객체를 반복해서 생성하는 것 보다는 재사용을 권장했다. 하지만 이것이 객체 생성은 비싸니 피해야한다로 받아들여지면 안된다. 요즘의 JVM에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다. 따라서 프로그램의 명확성, 간결성, 기능을 위해 객체를 추가로 생성하는 것은 일반적으로 좋은 일이다.
하지만 단순히 객체 생성을 피하고자 나만의 객체 풀을 만들지 말자. 일반적으로 자체 객체 풀은 코드를 헷갈리게 만들고 메모리 사용량을 늘리고 성능을 떨어뜨린다.
이펙티브 자바 3/E