Effective Java - Item 6. 불필요한 객체 생성을 피하라
객체를 매번 생성하기보다 재사용하는 경우가 좋을 때가 많다.
String t = new String("bikini"); // 1
String s = "bikini"; // 2
String cmpStr = "bikini"
if(t == s) ...
t = t.intern();
if(t == s) ...
Java 처음 배울 때 무조건 하는 예시이다.
1번 코드처럼 String 객체를 생성자로 생성하면 Heap 영역에 새로운 String 객체가 생성이 된다.
2번 코드와 같이 String 객체를 리터럴로 생성하면 Java의 String Pool 안에 생성된다.
이후에 cmpStr와 같이 리터럴로 String 객체를 생성하면 String Pool 안에서 bikini 객체를 찾고,
예전에 생성되어 있는 bikini 객체와 같은 레퍼런스를 가지게 된다.
String에는 intern()이라는 메소드가 존재하는데 이는 생성자로 Heap 영역에 할당한 String 객체가 존재한다면
이를 String Pool로 옮겨주는 메소드이다. Pool에 존재했다면 해당 레퍼런스를 반환하고, 없었다면 새로 생성해서 넣어준다.
/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();
}
어썸 한 native 코드이다 ㅎㅎ;
아이템 1의 static 팩토리 메소드를 통해 불필요한 객체 생성을 막을 수 있다.
예시로 Boolean(String)의 생성자를 통한 인스턴스 반환과 Boolean.valueOf(String)의 static 팩토리 메소드가 있다.
{
...
public static final Boolean TRUE = new Boolean(true);
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code false}.
*/
public static final Boolean FALSE = new Boolean(false);
...
public Boolean(String s) {
this(parseBoolean(s));
}
/**
* Parses the string argument as a boolean. The {@code boolean}
* returned represents the value {@code true} if the string argument
* is not {@code null} and is equal, ignoring case, to the string
* {@code "true"}. <p>
* Example: {@code Boolean.parseBoolean("True")} returns {@code true}.<br>
* Example: {@code Boolean.parseBoolean("yes")} returns {@code false}.
*
* @param s the {@code String} containing the boolean
* representation to be parsed
* @return the boolean represented by the string argument
* @since 1.5
*/
public static boolean parseBoolean(String s) {
return ((s != null) && s.equalsIgnoreCase("true"));
}
...
/**
* Returns a {@code Boolean} with a value represented by the
* specified string. The {@code Boolean} returned represents a
* true value if the string argument is not {@code null}
* and is equal, ignoring case, to the string {@code "true"}.
*
* @param s a string.
* @return the {@code Boolean} value represented by the string.
*/
public static Boolean valueOf(String s) {
return parseBoolean(s) ? TRUE : FALSE;
}
}
Boolean 객체 안에는 static final로 TRUE와 FALSE 값을 가지는 Boolean 객체의 인스턴스가 미리 생성돼 있다.
Boolean(String)을 통한 인스턴스 반환은 들어온 String 값이 "true"인지 "false"를 통해 Boolean의 생성자를 호출한다.
그러나 Boolean.valueOf(String) 메소드는 컴파일 단계에서 미리 생성되어 있던 TRUE 혹은 FALSE 객체를 반환한다.
해당 클래스가 immutable하게 설계되어 있다면 static 팩토리 메소드를 통한 인스턴스 반환을 사용할 수 있다.
생성 비용의 코스트가 큰 객체들도 더러 존재한다.
이런 객체의 사용이 빈번하게 일어난다면 이를 캐싱하여 사용하는 것이 좋다.
static boolean isRomanNumeralSlow(String s) {
return s.matches("^(?=.)M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
입력받은 문자열이 유효한 로마숫자임을 확인하는 메소드를 작성한다고 하자.
이 코드의 문제점은 s.matches(String)에 있다.
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
객체 지향을 적용해하는 일을 분기해 서로 다른 객체에게 시키는 코드이다. 맛있다! ㅋㅋ
matches 함수 내부에서 Pattern 객체와 Matcher 객체의 인스턴스가 새로 생성된다.
Pattern 객체는 입력받은 정규표현식을 유한 상태 머신(Finite State Machine)을 생성해서 인스턴스 생성 비용이 크다고 하다.
유한 상태 머신이 뭔지 검색했다가 후퇴했다. 그냥 그런갑다 해야겠다.
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();
}
Pattern 객체를 미리 캐싱해두고 함수가 호출될 때 캐싱된 인스턴스를 호출해 검사를 하면 성능이 야무지게 개선된다.
또한 static final로 선언하면 코드를 읽는 사람들이 명시적으로 의미를 확인하기 좋다는 장점도 있다.
static final로 인스턴스를 미리 생성해두면 만약 해당 인스턴스가 호출되지 않는다면 메모리 잡아먹는 금쪽이가 된다.
아이템 3에서 싱글톤 생성에 대한 이야기를 할 때 Lazy Initialization을 소개했다.
그러나 이는 웬만하면 하지 말라고 얘기한다. 잘 모르면서 최적화한다고 까불지 말라는 얘기를 한다.
다음은 어댑터에 대한 이야기이다.
Map<String, Integer> menu = new HashMap<>();
menu.put("Kimchi", 3);
menu.put("Zzigae", 5);
Set<String> names1 = menu.keySet();
Set<String> names2 = menu.keySet();
names1.remove("Zzigae");
System.out.println(names2 == names1); // true
System.out.println(menu.size()); // 1
Map의 Key 값들을 가지는 Set을 반환해 주는 keySet() 메소드는 호출 시마다 매번 새로운 인스턴스를 생성하는 것이 아니라 class 내부에 하나의 인스턴스를 가지고, 같은 인스턴스를 반환한다.
하나의 인스턴스만 가지고 있기에 set의 내부를 변경하면 map 또한 변경되게 된다.
또 다른 예로 오토박싱이 있다.
private static long sum() {
Long sum = 0L;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
sum을 long 타입의 wrapper class인 Long 타입으로 선언하면 아래의 코드에서 엄청난 성능저하를 일으킨다.
sum += i를 하는 코드에서 primitive long 타입을 Long 객체에 더 하기 위해선 long 타입을 Long 타입으로 변환하는 오토박싱이 적용된다. 새로운 Long 인스턴스가 생성되고 이를 sum에 다시 더할 때 새로운 Long 인스턴스가 생성된다.
sum을 primitive 타입으로 선언했을 땐 간단한 덧셈만이 발생하지만, wrapper 클래스인 Long으로 선언하면 불필요한 인스턴스가 21억 4천700만개가 생성된다.
이 파트의 핵심 내용은 객체의 생성을 최대한 피해라가 아니다.
무엇이든 코드의 가독성, 유지보수가 우선시되어야 한다. 요즘 JVM의 가비지 콜렉터는 최적화가 잘 돼있어 작은 객체의 생성과 회수가 큰 오버헤드로 작용하지 않는다.
객체를 재사용해서 발생하는 문제가 객체를 재사용하지 않아서 발생하는 성능 저하보다 큰 문제가 되곤 한다.
객체를 재사용할지 방어적 복사를 적용할지 잘 구분하는 것이 중요할 것 같다.