본문 바로가기

책리뷰/이펙티브자바

[이펙티브자바] 아이템6. 불필요한 객체 생성을 피하라

728x90
반응형

똑같은 기능을 하는 객체는 매번 생성하기보다 재사용하는 편이 빠르고 세련되다.

String s = new String("bikini");

String s = "bikini";

첫 번째 코드는 실행될 때마다 String 인스턴스를 새로 만들고 두 번째 코드는 하나의 String인스턴스를 재사용한다. 

 

정적 팩터리 메서드 사용


생성자 대신 정적팩터리 메서드를 사용하면 불필요한 객체 생성을 막을 수 있다. 

Boolean(String) 생성자 대신 Boolean.valueOf(String)팩터리 메서드 사용하는 것이 좋은 예이다.

(생성자는 자바9에서 deprecated됨)

 

생성 비용이 비싼 객체 사용 지양


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 객체를 만들어서 재사용 하는 것이 좋다. 

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 isRomanNumberal(String s) {
        return ROMAN.matcher(s).matches();
    }
}

정규표현식을 클래스 초기화 과정에서 생성해 캐싱해두고 이 인스턴스를 재사용하는 것이 좋다. 

isRomanNumeral이 빈번히 호출될 때 성능을 끌어올릴 수 있다. 

코드도 간결하고 명확해졌다. 

 

package item6;

import java.util.regex.Pattern;

public class Main {

    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})$");
    }
    public static void main(String[] args) {
        String s = "IV";
        long startTime = System.currentTimeMillis();
        for(int i = 0; i < 10000; i++) {
            isRomanNumeral(s);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("result : " + (endTime - startTime));

        startTime = System.currentTimeMillis();
        for(int i = 0; i < 10000; i++) {
            RomanNumber.isRomanNumberal(s);
        }
        endTime = System.currentTimeMillis();
        System.out.println("result : " + (endTime - startTime));
    }
}


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 isRomanNumberal(String s) {
        return ROMAN.matcher(s).matches();
    }
}

두 코드 속도를 비교해봤을 때 인스턴스 재사용하는 방법이 약 14배 정도 빨랐다.

 

그러나 이 코드도 isRomanNumeral이 호출되지 않으면 필요없이 만든셈이 되기 때문에 문제가 될 수 있다. 

 

 

어댑터


실제 작업은 뒷단 객체에 위임하고 자신은 제 2의 인터페이스 역할을 해주는 객체.

인터페이스를 통해 뒤에 있는 객체로 연결해주기 때문에 뒷단 객체 하나당 어댑터 하나씩만 만들면 된다. 

 

Map인터페이스의 keySet()메서드는 Map의 키 전부를 담은 Set뷰를 반환한다.

keySet을 호출할 때마다 새로운 객체가 나올 것 같지만 사실은 매번 같은 Set을 리턴한다.

Set타입의 객체를 변경할 경우 모든 객체가 따라서 바뀐다. 

따라서 keySet뷰를 여러개 만들어도 되지만 그럴 필요가 없다.

Map<String, Integer> map = new HashMap<>();
map.put("First", 1);
map.put("Second", 2);

Set<String> keys1 = map.keySet();
Set<String> keys2 = map.keySet();

keys1.remove("First");
System.out.println(keys1.size()); // 1
System.out.println(keys2.size()); // 1

 

 

오토박싱


오토박싱은 프리미티브 타입과 박스타입을 섞어쓸 경우 자동으로 박싱과 언박싱을 해준다. 

기본타입과 박스 타입의 경계를 안보이게 해주지만 경계가 없어지는 건 아니다.

long start = System.currentTimeMillis();
Long sum = 0L;
for(long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i;
}
System.out.println(System.currentTimeMillis() - start); // 3329

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

long start = System.currentTimeMillis();
long sum2 = 0L;
for(long i = 0; i < Integer.MAX_VALUE; i++) {
    sum2 += i;
}
System.out.println(System.currentTimeMillis() - start); // 680

박싱보다는 기본타입을 사용하고 의도치 않은 오토박싱을 피해야 한다. 

 

이번 아이템으로 인해 객체 생성은 비싸니 피해야 한다로 오해하면 안된다. 

프로그램의 명확성, 간셜성, 기능을 위해 객체를 추가 생성하는 것은 좋은 일이다. 

 

단순히 객체 생성을 피하고자 객체 풀을 만들 필요는 없다. 

 

불필요한 객체 생성보다 방어적 복사에 실패하는 경우가 더 위험하다.(이건 아이템 50에서..)

728x90
반응형