DEV ℧ Developer Diary

[EffectiveJava] item61 - 박싱된 기본 타입보다는 기본 타입을 사용하라

자바의 데이터 타입은 크게 두가지로 나누어 진다.

  • 기본타입 : int, double, boolean…
  • 참조타입 : String, List…

각각의 기본 타입에는 대응하는 참조 타입이 하나씩 있으며, 이를 박싱된 기본 타입이라고 한다. int, double, boolean에 대응하는 박싱된 기본타입은 Integer, Double, Boolean 이다.

박싱된 기본타입

오토박싱과 오토언박싱 덕분에 두타입을 크게 구분하지 않고 사용할 수는 있지만, 차이가 사라지는 것은 아니다.

둘사이에는 분명한 차이가 있으므로 어떤 타입을 사용할지는 상당히 중요하다.

기본 타입과 박싱된 기본 타입의 차이

식별성의 유무

기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값에 더해 식별성(identity)이란 속성도 갖는다. 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별 될 수 있다.

null의 유무

기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않은 값 null을 가질 수 있다.

메모리의 효율

기본타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.

박싱된 기본타입의 문제점 - 식별성의 유무

예시를 톨해 둘의 차이를 알아보도록 하자. 다음은 Integer 값을 오름차순으로 정렬하는 비교자다.

잘못 구현된 비교자

Comparator<Integer> naturalOrder =
  (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

별다른 문제를 찾기가 어렵고, 실제로 이것저것 테스트를 진행해도 잘 통과한다.

해당 예시의 결함을 확인 하고 싶다면 naturalOrder.compare(new Integer(42), new Integer(42))의 값을 확인 해보자.

두 integer 인스턴스의 값이 42로 같으므로 0을 출력해야 하지만, 실제로는 1을 출력한다.

원인

원인을 살펴보도록 하자. natureOrder의 첫번째 검사 (i < j)는 잘 작동한다. 여기서는 i와 j가 참조하는 오토박싱된 Interger 인스턴스는 기본 타입 값으로 변환 된다.

만약 i가 j보다 작지 않다면 두 번째 검사인 (i == j)가 이뤄지는데, 이 두 번째 검사는 i와 j의 값이 아닌 두 ‘객체 참조’의 식별성을 검사하게 된다.

i와 j가 서로 다른 Integer 인스턴스라면 값이 같더라도 false의 결과가 나온다. 박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어난다.

기본 타입의 비교

실무에서 이와 같이 기본 타입을 다루는 비교자가 필요하다면 Comparator.naturalOrder()를 사용하자. 비교자를 직접 만들면 비교자 생성 메서드나 기본 타입을 받는 정적 compare 메서드를 사용해야 한다.

박싱된 Integer 매개변수 i, j 값을 기본 타입 정수로 저장한 다음, 모든 비교를 이 기본 타입 변수로 수행하면 오류의 원인인 식별성 검사가 이뤄지지 않는다.

Comparator<integer> naturalOrder = (iBoxed, jBoxed) -> {
    int i = iBoxed, j = jBoxed; /** 오토박싱 */
    return i < j ? -1 : (i == j ? 0 : 1);
}

박싱된 기본타입의 문제점 - null 참조

또 다음 코드를 살표보도록 하자.

public class Unbelievable {
    static Integer i;

    public static void main(String[] args) {
        if (i == 42)
            System.out.println("믿을 수 없다!");
    }
}

이 프로그램은 아래의 “믿을 수 없다!” 를 출력하지 않고 i == 42를 검사 할때 NullPointerException을 던진다.

원인

변수 i의 타입이 int가 아닌 integer이며, 다른 참조 타입 필드와 마찬가지로 i의 초기값도 null이라는 데 있다. 즉, i == 42는 Integer와 int를 비교하는 것이다.

거의 예외 없이 기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 박싱된 기본타입의 박싱이 자동으로 풀린다. 그리고 null 참조를 언박싱 하면 NullPointerException이 발생한다.

박싱된 기본타입의 문제점 - 성능

끔찍이 느리다!

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

이 프로그램은 실수로 지역변수 sum을 박싱된 기본 타입으로 선언하여 느려졌다. 오류나 경고 없이 컴파일 되지만, 박싱과 언박싱이 반복해서 일어나 체감될 정도로 성능이 느려진다.

박싱된 기본 타입이 올바른 예시

박싱된 기본 타입은 언제 써야 할까?

컬렉션의 원소, 키 값

컬렉션은 기본 타입을 담을 수 없으므로 어쩔 수 없이 박싱된 기본 타입을 써야만 한다.

매개변수화 타입, 매개변수화 메서드

매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 써야 한다. 자바 언어가 타입 매개변수로 기본 타입을 지원 하지 않기 때문이다.
예를 들면 ThreadLocal 타입으로 선언 하는것은 불가능 하고, ThreadLocal를 써야 한다.

리플렉션

리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야 한다.