Language/Java

[effective java] 아이템 6. 불필요한 객체 생성을 피하라.

JOYERIM 2023. 3. 1. 20:25

 

 

 

 

 

객체를 새로 만드는 것보다 객체 하나를 재사용하는 것이 대부분 적절하다.

특히 불변 객체는 언제든 재사용할 수 있다.

또한, 가변 객체여도 사용 중에 변경되지 않을 경우 재사용할 수 있다.

 

 

 

 

문자열 객체 생성

 

 

아래 코드는 매번 String 인스턴스를 새로 만든다.

String s = new String("yerim");

 

반면 아래 코드는 하나의 String 인스턴스를 사용한다.
즉, 문자열 리터럴을 재사용하기 때문에 해당 자바 가상 머신에 동일한 문자열 리터럴이 존재한다면 그 리터럴을 사용한다.

 

String s = "yerim";

 

 

 

 

static 팩토리 메소드

 

 

자바 9에서 deprecated된 Boolean(String) 생성자 대신 Boolean.valueOf(String) 팩터리 메소드를 사용하자.

 

 

 

 

 

값이 비싼 객체

 

생성 비용이 비싼 객체가 반복해서 필요하다면 캐싱하여 재사용하길 권장한다.

 

그 예로 정규 표현식을 활용한 문자열이 로마 숫자인지 확인하는 코드를 살펴보자.

 

    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.mathes가 가장 쉽게 정규 표현식에 매치가 되는지 확인하는 방법이긴 하지만 성능이 중요한 상황에서 반복적으로 사용하기에 적절하지 않다.

 

String.mathes는 내부적으로 Pattern 객체를 만드는데, 그 객체를 만드려면 정규 표현식으로 유한 상태 기계로 컴파일 하는 과정이 필요하기 때문이다.

 

따라서 성능을 개선하려면 Pattern 객체를 만들어 재사용하는 것이 좋다.

 

public class RomanNumber {

    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();
    }

}

 

그러나 isRomanNumeral 메소드가 호출되지 않는다면 ROMAN는 쓸데없이 초기화된 것이다. 이를 게으른 초기화(아이템 83)를 사용해서 초기화를 없앨 순 있으나 권장하지 않는다. 코드는 복잡하고, 성능이 크게 개선되지 않기 때문..

 

 

 

 

어댑터
얘도 다시 공부하자..

 

객체가 불변인 경우 재사용하는 것이 명확하지만, 그렇지 않은 경우가 있다. 그 예시가 어댑터이다.

어댑터는 인터페이스를 통해서 뒤에 있는 객체로 연결해주는 객체라 여러개 만들 필요가 없다.

 

책에서는 어댑터와 비슷한 구조로 아래 예시를 사용하였다.

Map 인터페이스가 제공하는 keySet은 Map이 뒤에 있는 Set 인터페이스의 뷰를 제공한다.

아래 코드를 보면 menu.keySet() 메소드를 호출하여 Map의 key 값들을 Set으로 받아온다. 이렇게 받아온 Set은 Map의 Key 값들을 참조하고 있으므로, Set에 속한 Key 값들의 변화는 Map에도 영향을 준다.

 

names1과 names2 변수가 같은 Map 인스턴스의 Key 값을 참조하고 있기 때문에, names1에서 Key 값 "Burger"를 삭제하면, names2의 크기와 Map의 크기도 모두 1이 된다. 따라서, 이 예제에서는 Map의 Key 값을 수정할 때, Key Set을 복사하여 사용하거나, 수정할 Key 값을 직접 참조하는 것이 좋다.

 

public class UsingKeySet {

    public static void main(String[] args) {
        Map<String, Integer> menu = new HashMap<>();
        menu.put("Burger", 8);
        menu.put("Pizza", 9);

        Set<String> names1 = menu.keySet();
        Set<String> names2 = menu.keySet();

        names1.remove("Burger");
        System.out.println(names2.size()); // 1
        System.out.println(menu.size()); // 1
    }
}

 

아래 코드는 keySet() 리턴 값을 HashSet 클래스의 생성자로 전달하여 불변 복사본을 생성한 예다.

 

public class UsingKeySet {

    public static void main(String[] args) {
        Map<String, Integer> menu = new HashMap<>();
        menu.put("Burger", 8);
        menu.put("Pizza", 9);

        Set<String> names1 = new HashSet<>(menu.keySet());
        Set<String> names2 = new HashSet<>(menu.keySet());

        names1.remove("Burger");
        System.out.println(names2.size()); // 2
        System.out.println(menu.size()); // 2
    }
}

 

*어댑터

 

 

 

 

오토박싱

 

 

아래 코드는 long 타입인 i가 Long 타입인 sum에 더해질 때마다 불필요한 Long 인스턴스가 만들어진다. 

 

public static void main(String[] args) {
        Long sum = 0L;
        for (long i = 0 ; i <= Integer.MAX_VALUE ; i++) {
            sum += i;
        }
        System.out.println(sum);
}

 

따라서 불필요한 오토박싱을 피하려면 박스 타입보다는 기본 타입을 사용하도록 하자.

 

 

 

 

요약

 

 

무작정 객체 생성을 피하라는 것이 아니다. 예를 들어 방어적 복사(아이템 50)이 필요한 경우 객체를 재사용하면, 심각한 버그와 보안성의 문제가 생긴다. 그에 비해 객체 생성은 코드 형태와 성능에만 영향을 주기 때문에 잘 판단하여 사용하자.