new 대신 리터럴을 사용하자
String s = new String("대우리는daewoolee가아니라daewoole");
Java
복사
매우 나쁜 예시
이 코드가 실행될 때마다 매 번 새로운 문자열 객체를 생성하게 된다.
물론 스코프를 벗어나면 GC에 의해 회수되겠지만 굳이 불필요한 객체를 생성할 필요는 없을 것이다.
String s = "대우리는daewoolee가아니라daewoole";
Java
복사
개선된 버전
리터럴을 사용하면 모든 코드가 같은 객체 사용이 보장된다. (JLS, 3.10.5)
이 방식을 사용하면 불필요한 객체 생성을 막을 수 있다.
생성자 대신 팩토리 메소드
String s = "오늘안온박재범씨";
Boolean(s);
Java
복사
생성자를 사용한 불필요한 객체 생성
형변환을 위해 Boolean의 생성자를 이용하고 있다.
이 역시 불필요하게 true나 false 값을 갖는 Boolean 객체를 매 번 만들게되어 비효율적이다.
자바 9부터 deprecated API로 지정 됐을 정도이니, 굳이 사용할 필요가 없는 방식이다.
String s = "오늘안온박재범씨";
Boolean.valueOf(s);
Java
복사
Boolean.valueOf를 이용하게 되면 미리 static final로 지정된 값을 리턴시켜준다.
실제 java.lang에 작성된 Boolean 클래스 코드의 일부이다.
static final로 TRUE와 FALSE를 정의해두고, valueOf라는 팩토리 메소드가 호출되면 미리 정의한 값을 반환하는 것을 확인할 수 있다.
비싼 객체는 캐싱해서 사용하자
static boolean isRomanNumberal(String s) {
return s.matches("M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})");
}
Java
복사
한 번 쓰고 바로 버려질 Pattern 인스턴스
isRomanNumberal은 String.matches 메소드를 이용해 제시된 문자열이 로마 문자열 패턴과 일치하면 true를 반환하는 메소드이다.
그런데 String.matches는 이런식으로 정의되어 있다.
java.util.regex.Pattern이라는 클래스의 matches라는 팩토리 메소드를 호출한다.
그런데 Pattern.matches에서 Pattern.compile이라는 팩토리 메소드를 다시 호출하고,
여기서 new Pattern()으로 새로운 Pattern 객체를 생성한다.
즉, 이 메소드가 호출될 때마다 불필요하게 Pattern 객체가 새로 생성되었다가 회수되어버린다..!
public class RomanNumberals {
private static final Pattern ROMAN = Pattern.compile(
"^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumberal(String s) {
return ROMAN.matcher(s).matches();
}
}
Java
복사
Pattern 인스턴스를 정적으로 초기화
RomanNumberals라는 클래스를 만들어두고, 이 안에 static final로 Pattern 인스턴스를 정적으로 초기화해서 캐싱해두고, isRomanNumberal 메소드가 호출하게 되면 여러 번 메소드를 호출하더라도 미리 캐싱해두었던 Pattern 객체를 재사용할 수 있다..!
저자의 상황에서는 길이가 8인 문자열을 입력했을 때, 개선 전 1.1us에서 개선 후 0.17us로 무려 6.5배정도 빨라졌다고 한다!
Pattern 인스턴스가 static final 필드로 빠짐으로써 의미도 더 잘 드러나기 때문에 여러모로 장점을 가지는 방식이다.
isRomanNumeral이 처음 호출될 때 필드를 초기화시키는 Lazy initialization을 이용하여 불필요한 초기화를 없앨 순 있지만 성능이 크게 개선되진 않아서 권장되지 않는다.
Lazy initialization이란?
오토박싱
오토박싱은 프로그래머가 기본 타입(int)과 박싱된 기본 타입(Integer)를 섞어서 쓸 때 자동으로 상호 변환해주는 기술을 말한다.
의미상으론 기본 타입과 박싱된 기본 타입을 섞어써도 차이가 없지만 성능면에서는 차이가 매우 크다..!
private static long sum() {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
Java
복사
오토박싱이 성능을 크게 떨어뜨리는 예시 코드
1부터 Integer 최대값(2,147,483,647)부터 더해서 Long으로 선언된 sum 변수에 저장하는 예시이다.
그런데 sum은 박싱 타입인 Long으로 정의되어 있고, i는 for문 scope 내에서 원시 타입인 long으로 정의되어 있다.
즉, sum += i를 호출할 때마다 오토박싱이 발생한다.
다시 말해 오토박싱이 낮게 잡아도 21억 번이 발생한다는 것이다..!
저자는 단순히 sum을 Long 대신 long으로 정의하는 방식으로 수정만 했을 뿐인데,
6.3초에서 0.59초로 크게 성능을 개선시켰다고 말한다!!
주의
객체 생성은 비싸니까 만들지 말아야지~
→ 이건 잘못된 말이다.
요즘 JVM의 성능이 발전해서 primitive한 작은 객체를 생성하고 회수하는 일은 크게 부담되지 않는다.
조금 더 명확하고 간결하고 이해하기 쉬운 코드를 짜기 위해 객체를 추가로 생성하는 것은 오히려 좋다고 평가할 수도 있다!
또한 단순히 오로지 객체 생성을 피하려는 목적으로만 객체 pool을 만들어 관리하는 것은 좋지 않다.
일반적인 상황에서는 괜히 메모리 사용양을 늘리고 성능을 떨어뜨리고, 코드 작성을 어렵게 만들게 할 수 있다.
객체 pool을 만들어 관리하는게 좋은 상황도 있긴하다!
ex. DB 연결을 위한 객체같이 워낙 비용이 비싼건 재사용하는게 좋다.
Item 6는 Item 50(새로운 객체를 만들어야 한다면 기존 객체를 재사용하지 마라)의 방어적 복사(defensive copy)와는 조금 대조적인 Item이다.
방어적 복사에 실패하면 버그 펑펑 보안 문제. but 불필요한 객체 생성은 코드 형태와 성능에만 문제.