도당탕탕

Item61 : 박싱된 기본 타입보다는 기본 타입을 사용하라 본문

JAVA

Item61 : 박싱된 기본 타입보다는 기본 타입을 사용하라

backlo 2023. 1. 16. 15:26

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

  1. 원시 타입 : int, double, boolean
  2. 참조 타입 : Integer, Double, Boolean

오토박싱과 오토언박싱 덕분에 두 타입을 크게 구분하지 않고 사용할 수는 있지만, 그렇다고 차이가 사라지는 것은 아니다. 둘 사이의 분명한 차이는 있으니 적절히 사용해야 한다.

기본 타입과 박싱 된 기본 타입의 주된 차이는 크게 세 가지이다.

  1. 기본 타입은 값만 가지고 있으나, 박싱 된 기본 타입은 값에 더해 식별성이란 속성을 갖는다. 달리 말하면 박싱된 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별될 수 있다.
  2. 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않는 값, 즉 null을 가질 수 있다.
  3. 기본 타입이 박싱된 기본타입보다 시간과 메모리 사용면에서 더 효율적이다.

이 세 가지를 주의하지 않고 사용하면 진짜로 문제가 발생할 수 있다.

식별성

  • 잘못 구현된 비교자
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

위 코드에서 웬만하면 버그 없이 코드가 잘 돌아간다. 하지만 다음과 같은 경우 어떻게 될까?

naturalOrder.compare(new Integer(42), new Integer(42))

두 인스턴스의 값이 42이므로 0을 출력해야 할 것 같지만, 실제로는 1을 출력한다. 그럼 이유가 뭘까?

바로 i==j 이 부분이다. 동일성 비교연산자는 원시 타입에서 서로의 값을 비교해 주지만 참조 타입 같은 경우 서로 객체가 같은지 판별해 준다. 즉 참조 타입의 값이 서로 같더라도 객체가 서로 다르기 때문에 1을 출력하는 것이다. 이처럼 박싱 된 기본 타입에 ==연산자를 사용하면 오류가 일어난다.

실무에서는 위와 같이 비교 코드를 만들지 말고 Comparator.naturalOrder()를 사용하자. 하지만 만약 만들어서 해결해야 한다면 다음과 같다.

  • 문제를 수정한 비교자
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
  int i = iBoxed, j = jBoxed; // 오토박싱
  (i < j) ? -1 : (i == j ? 0 : 1);
}

null 가능성

  • 기이하게 동작하는 프로그램
public class Unbelievable {
        static Integer i;

    public static void main(String[] args) {
        try {
            if (i == 42) // i 객체가 null (0이 아니다.)
                System.out.println("믿을 수 없군!");
        } catch (NullPointerException e) {
            i = new Integer(42);
            if (i == 42) // 참조타입과 원시타입 비교할 시 자동으로 언박싱 진행 
                System.out.println("믿을 수 없군!");
        }
    }
}

이 프로그램도 마찬가지로 i가 참조 타입이기 때문에 i==42 비교하는 순간 NullpointException이 발생할 것이다.

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

이 예에서 보듯, 이런 일은 어디서든 벌어질 수 있다. 해법은 간단한데 바로 Integer를 int로 고치면 된다.

성능

  • 끔찍이 느린 코드
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을 참조 타입으로 선언하여 느려졌다. 오류나 경고 없이 컴파일되지만, 박싱과 언박싱이 반복해서 일어나 체감될 정도로 성능이 느려진다.

정리

이렇게 기본 타입과 박싱 된 기본 타입의 차이를 무시하고 구현할 시 나타날 문제들을 살펴 보았다. 그럼 언제 박싱된 기본 타입을 써야 하는가?

  1. 컬렉션의 원소, 키, 값으로 쓴다. 컬렉션은 기본 타입을 담을 수 없으므로 어쩔 수 없이 박싱된 기본 타입을 써야만 한다.
  2. 리플랙션을 통해 메서드를 호출할 때도 박싱 된 기본 타입을 사용해야 한다.

이런 상황일 때 사용하면 좋다.

Comments