똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.
String s = new String("Kyummi");
해당 코드는 실행될 때마다 String 인스턴스가 새로 만들어진다.
단점 : 반복문이나 많이 호출되는 메서드 안에 있다면, 쓸데없는 String 인스턴스가 생기게된다.
String s = "Kyummi";
해당 코드는 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용
++ 같은 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장
String을 Boolean으로 변환해주는 생성자 및 함수 존재 "true"(대소문자 구분 X)는 true 아니면 false를 반환
- Boolean(String)
- Boolean.valueOf(String)
Boolean b = new Boolean("true");
호출할 때마다 새로운 객체 생성
자바 9에서 사용자제(deprecated) API로 지정
Boolean b = Boolean.valueOf("true");
팩터리 메서드 사용 -> 재사용
불변 객체만이 아니라 가변 객체라 해도 사용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.
생성 비용이 클 경우, 반복해서 필요하다면 캐싱하여 재사용하길 권한다.
주어진 문자열이 유효한 로마 숫자인지 확인하는 메서드를 만든다고 하자
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
는 정규표현식으로 문자열 형태를 확인하는 가장 쉬운 방법이지만, 성능이 중요한 상황에서 반복해 사용하기엔 적합하지 않다.
WHY?
메서드 내부에서 만드는 정규표현식용 Pattern 인스턴스 생성 비용 ↑
Pattern 인스턴스는 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다. → 즉 매번 다시 만들어줘야 함
Pattern 인스턴스는 입력받은 정규표현식에 해당하는 유한 상태 머신(FSM; finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다.
유한 상태 머신
장치나 모델이 가질 수 있는 유한 개의 상태를 정의하고, 조건에 맞는 이벤트가 발생하면 해당 상태 변경되는 방법으로 동작하는 것 (이벤트에 따라 상태가 계속 변화한다.)
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 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeral 메서드가 호출될 때마다 이 인스턴스 재사용
변경된 코드가 성능 향상 뿐만 아니라 코드도 더 명확하다.
성능 향상 정도
effectiveJava 저자가 컴퓨터에서 길이가 8인 문자열을 입력했을 때
String.matches : 1.1μs (마이크로초) Pattern 캐싱해서 사용 : 0.17μs
6.5배 빠름 ~!
사용되지 않을 때는 초기화 하고 싶지 않다면 지연 초기화(laze initialization)도 있긴 하다.
But 권하지 않는다. 코드가 더 복잡해지는거에 비해 성능이 크게 개선되지 않을 때가 많기 때문이다.
지연 초기화(laze initialization): 메서드가 처음 호출될 때 필드를 초기화 하는 방식
ex)
public class RomanNumerals { private static Pattern ROMAN = null; static boolean isRomanNumeral(String s) { if(ROMAN == null) { ROMAN = Pattern.compile( "^(?=.)M*(C[MD]D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"); } return ROMAN.matcher(s).matches(); } }
어댑터 : 실제 작업은 뒷단 객체에 위임하고 자신은 제 2의 인터페이스 역할을 해주는 객체 (즉 연결을 돕는 객체)
HashMap<Integer, Boolean> map = new HashMap<>();
map.put(1, true);
map.put(2, false);
Set<String> keys1 = map.keySet();
Set<String> keys2 = map.keySet();
해당 코드에서 map 데이터가 뒷단 객체이고 key1, key2가 어댑터라고 생각해라.
map.keySet() 메서드 실행 될 때마다, 새로운 Set을 생성할까?
No! 똑같은 데이터를 반환한다.
→ 재사용 (반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀐다.)
: 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술
오토박싱은 기본 타입과 그에 대응하는 박싱 된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.
private static long sum() {
Long sum = 0L;
for(long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
-> 속도가 오래 걸린다. : long(i)을 Long(sum)으로 오토박싱이 계속 이뤄지기 때문이다.
Why? long이 아닌 Long으로 선언해서 불필요한 Long 인스턴스가 약 2^31개나 만들어진 것이다.
(Long 일 때) 6.3초 -> (long 일 떄) 0.59초
박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱에 숨어들지 않도록 주의하자.