[Effective JAVA] Item6. 불필요한 객체 생성을 피하라

보람찬하루·2024년 5월 4일
0

Effective JAVA

목록 보기
6/8

배경

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


String s = new String(‘'bikini"); // 1번

VS

String s = "bikini";  //2번

1번의 경우 실행될 때 마다 String 인스턴스를 새로 만들고 2번은 하나의 인스턴스를 사용한다. 1번처럼 new 키워드를 이용해 문자열을 생성하는 것은 무의미하며 성능에 악영향을 끼친다. 2번의 방식을 사용한다면 같은 vm안에서 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.


불필요한 객체 생성 대신 정적 팩터리 메서드를 사용하여 불필요 객체의 생성을 피하자.

//1번 - **Boolean.valueOf(String) 사용**
Boolean boolean1 = Boolean.valueOf("true");
Boolean boolean2 = Boolean.valueOf("true");

System.out.println(boolean1 == boolean2);

//2번 - **Boolean(String) 사용**
Boolean boolean3 = new Boolean("true");
Boolean boolean4 = new Boolean("true");

System.out.println(boolean3 == boolean4);

여기서도 2번의 방식을 사용하는 것이 기능적으로 같은 역할을 하는 객체를 반복적으로 생성할 필요가 없기 때문에 더 좋다.


객체 생성비용이 비싸다면 객체를 재사용하자.

객체 생성비용이 비싼 객체가 반복적으로 필요하다면 캐싱해서 재사용하는것이 좋다.

[문제 - 재사용빈도가 높고 생성비용이 비싼 경우 - 정규표현식 사용]

public 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 메서드를 사용하는데 있다. 이 메서드가 내부에서 만드는 Pattern 인스턴스는, 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다. Pattern 은 입력받은 정규표현식에 해당하는 유한상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다.

[해결]

성능을 개선을 위해 정규표현식을 표현하는 (불변) Pattern 인스턴스를 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 나중에 isRomanNumeral 메서드가 호출될 때마다 이 인스턴스를 재사용하게 하면된다.

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})$");

public static boolean isRomanNumeral(String s) {
    return ROMAN.matcher(s).matches();
}

불필요한 객체 재사용은 지양하자.

[같은 인스턴스를 대변하는 여러개의 인스턴스를 생성]

final Map<String, Integer> beverage = new HashMap<>();

beverage.put("coke", 10);
beverage.put("cider", 9);
beverage.put("water", 8);

Set<String> keySet1 = beverage.keySet();
Set<String> keySet2 = beverage.keySet();

System.out.println("beverage Size: " + beverage.size());
System.out.println("keySet1 Size: " + keySet1.size());
System.out.println("keySet2 Size: " + keySet2.size());

keySet1.remove("water");

System.out.println("beverage Size: " + beverage.size());
System.out.println("keySet1 Size: " + keySet1.size());
System.out.println("keySet2 Size: " + keySet2.size());
// 실행결과
beverage Size: 3
keySet1 Size: 3
keySet2 Size: 3

beverage Size: 2
keySet1 Size: 2
keySet2 Size: 2

반환된 Set인스턴스가 가변일지라도 반환된 인스턴스들은 기능적으로 모두 같다. 즉, 반환한 객체 중 하나를 수정하면 위의 코드처럼 다른 모든 객체가 따라서 바뀐다.

따라서 keySet이 뷰 객체를 여러개 만들 필요도 없고 이득도 없다.


[의도치 않은 오토박싱 객체 생성]

public static void main(String[] args) {
    long start = System.currentTimeMillis();
    Long sumAutoboxing = sumAutoboxing();

    System.out.println("sumAutoboxing: "+sumAutoboxing);
    System.out.println(System.currentTimeMillis()- start);

    start = System.currentTimeMillis();
    long sumPrimitive = sumAutoboxing();

    System.out.println("sumPrimitive: "+sumPrimitive);
    System.out.println(System.currentTimeMillis()- start);
}

private static long sumAutoboxing() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++) {
        sum += i;
    }

    return sum;
}

private static long sumPrimitive() {
    long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++) {
        sum += i;
    }

    return sum;
}
// 수행결과
sumAutoboxing: 2305843008139952128
6553
sumPrimitive: 2305843008139952128
5638

위의 오토박싱의 경우 sum 변수를 Long으로 선언해서 불필요한 Long 인스턴스가 만들어지기 때문에 long 타입으로 선언했을떄 보다 훨씬 느리다.

따라서, 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어있지 않도록 주의해야 한다.


핵심정리

무조건 객체를 재사용할 것이 아니라 상황에 따라 객체 재사용 여부를 판단하여 객체를 재사용해야 한다.

profile
를 만들어 가자

1개의 댓글

comment-user-thumbnail
2024년 5월 7일

이펙티브 자바 너무 어려워 보이던데 잘 정리 하셨네요 !!
저두 남궁성의 자바 끝내고 이펙티브 자바 볼까하는데 종종 참조하러 오겠습니답 ㅎㅎ

답글 달기