- 똑같은 기능의 객체를 매번 생성하기 보다는 재사용하는 편이 나을 것
- 특히 불변 객체는 언제든 재사용할 수 있다.
String s = new String("bikini");
- 위의 문장은 실행될 때마다 String 인스턴스를 만듦
- 위의 문장이 반복문 안에서 100만번 호출된다면?
- 쓸데 없는 String 인스턴스가 백만개일수도..
String s = "bikini";
- 이 코드는 위 코드의 개선 버전
- 새로운 인스턴스를 매번 만드는 대신 하나의 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})$");
- 위의 코드에서
String.matches
를 사용하는 부분을 보자. String.matches
는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법String.matches
동작 중 내부적으로 정규표현식용 Pattern 인스턴스를 만든다.- But 패턴 인스턴스 생성이 무거운 작업이기에 성능이 중요한 상황에 쓰기 쉽지 않다.
- 이러한 메서드가 사용될때마다 패턴 인스턴스가 생성되고 삭제된다면 그 비용은 만만치 않을 것
public class RomanNumerals {
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이 불변이라는 점을 이용해서 캐싱해두었다.
- 그리고 나중에 이 메서드가 호출될 때마다 인스턴스가 재활용되니 우아하다.
- 객체가 불변이라면 재사용해도 안전함이 명백하다.
- 위의 예시에서 패턴 인스턴스를 재활용한 것도 정적으로 선언후에 변하지 않을 것임을 믿기 때문
- 이러한 개념은 우리의 직관으로 충분히 받아들일 수 있다.
- 하지만 불변하다는 특성이 매번 재사용하기에 안전하다는 특성으로 귀결되지는 않는다.
- 오히려 반대인 상황도 있음
- 어댑터를 생각해보자
- 어댑터는 실제 작업은 뒷단 객체만 관리
- 뒷단 객체 외에는 관리할 대상 없음 (불변)
- 이렇게 보면 뒷단 객체 하나당 어댑터 하나씩만 만들어지면 충분해보임
public class UsingKeySet {
public static void main(String[] args) {
Map<String, Integer> menu = new HashMap<>();
menu.put("Burger", 8);
menu.put("Pizza", 9);
Set<String> names1 = menu.keySet();
Set<String> names2 = menu.keySet();
names1.remove("Burger");
System.out.println(names1.size()); // 1
System.out.println(names2.size()); // 1
}
}
- 실제로 Map 인터페이스의 keySet 메서드는 Map 객체 안의 키 전부를 담은 Set 뷰를 반환한다.
- KeySet은 어댑터처럼 오직 하나, 불변한 자신 뒤의 Map만 바라보기에 KeySet을 개발한 개발자는 재사용을 고려하였을지도 모르겠다. (실제로 재사용)
- 그런데 사용하는 개발자들은 keySet을 호출할 때다마 새로운 Set 인스턴스가 만들어지리라고 순진하게 생각할 수도 있음
- 하지만 그렇지 않다
- 매번 같은 Set 인스턴스를 반환함 (재사용)
- 그리고 위처럼 변경에 취약하게 되었다.
- keySet이 생성하는 셋뷰는 하나의 대상만을 바라보기에 어댑터와 마찬가지로 하나의 인스턴스만 필요할 것 처럼 사료된다.
- 하지만 직관적이지 않고 불필요한 동작을 담아내는 안전하지 않은 특성으로 귀결되는 현상을 확인할 수 있다.
- 오토 박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려준다.
- 하지만 완전히 없애주는 것은 아니다.
- 의미상으로는 별다를 것 없지만 성능에서는 그렇지 않다.
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
- 이 프로그램이 정확한 답을 내기는 한다.
- 하지만 제대로 구현했을 때보다 훨씬 느리다.
- sum 변수를 Long으로 선언해서 불필요한 Long 인스턴스가 약 2^31개나 만들어진 것..
- 결론은? 의도치 않은 오토박싱이 숨어들지 않도록 주의하자.
- 요즘의 jvm에서는 별다른 일을 하지 않는 작은 객체를 생성하고 회수하는 일이 크게 부담되지 않는다.
- 프로그램의 명확성, 간결성, 기능을 위해서 객체를 추가로 생성하는 것이라면 일반적으로 좋은 일이다.
- 거꾸로 아주 무거운 객체가 아닌 다음에야 단순히 객체 생성을 피하고자 객체 풀을 만들지는 말자. (별도의 얘기로 풀링과 캐싱의 차이점은 한번씩 찾아보시길)
- 이번 아이템은 기존 객체를 재사용할 수 있다면 재사용하라는 이야기
- 하지만 아이템 50은 새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라다.
- 사실 방어적 복사가 필요한 상황에서 객체를 재사용했을 때의 피해가 더 크다. (버그와 보안구멍으로 이어짐)
- 불필요한 객체 생성은 그저 코드 형태와 성능에만 영향을 주니 참고하라
https://deveric.tistory.com/123
이펙티브 자바 (조슈아 블로크) 아이템 06 불필요한 객체 생성을 피하라