Skip to content

Latest commit

 

History

History
212 lines (143 loc) · 6.24 KB

불필요한_객체_생성을_피하라_켬미.md

File metadata and controls

212 lines (143 loc) · 6.24 KB

item 6. 불필요한 객체 생성을 피하라

똑같은 기능의 객체를 매번 생성하기보다는 객체 하나를 재사용하는 편이 나을 때가 많다.

불변 객체일 경우, 재사용이 좋은 예시

💡 String 객체 예시

new String("") ← X
String s = new String("Kyummi");

해당 코드는 실행될 때마다 String 인스턴스가 새로 만들어진다.

단점 : 반복문이나 많이 호출되는 메서드 안에 있다면, 쓸데없는 String 인스턴스가 생기게된다.


"" ← O
String s = "Kyummi";

해당 코드는 새로운 인스턴스를 매번 만드는 대신 하나의 String 인스턴스를 사용

++ 같은 가상 머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장


💡 Boolean 객체 예시

String을 Boolean으로 변환해주는 생성자 및 함수 존재 "true"(대소문자 구분 X)는 true 아니면 false를 반환

  • Boolean(String)
  • Boolean.valueOf(String)

new Boolean("") ← X
Boolean b = new Boolean("true");

호출할 때마다 새로운 객체 생성

자바 9에서 사용자제(deprecated) API로 지정


Boolean.valueOf("") ← O
Boolean b = Boolean.valueOf("true");

팩터리 메서드 사용 -> 재사용


🤔 불변 객체만 재사용?

불변 객체만이 아니라 가변 객체라 해도 사용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.

💡 생성 비용이 큰(비싼) 객체 - Ex. 정규표현식 확인

생성 비용이 클 경우, 반복해서 필요하다면 캐싱하여 재사용하길 권한다.

주어진 문자열이 유효한 로마 숫자인지 확인하는 메서드를 만든다고 하자


String.matches ← X
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)을 만들기 때문에 인스턴스 생성 비용이 높다.


유한 상태 머신

장치나 모델이 가질 수 있는 유한 개의 상태를 정의하고, 조건에 맞는 이벤트가 발생하면 해당 상태 변경되는 방법으로 동작하는 것 (이벤트에 따라 상태가 계속 변화한다.)

더 자세한 설명


Pattern 캐싱하기 ← O
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! 똑같은 데이터를 반환한다.

→ 재사용 (반환한 객체 중 하나를 수정하면 다른 모든 객체가 따라서 바뀐다.)


💡 오토박싱 (auto boxing)이 일어나지 않게 해라

: 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술

오토박싱은 기본 타입과 그에 대응하는 박싱 된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.

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초

박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱에 숨어들지 않도록 주의하자.