본문 바로가기

개발/이펙티브 자바

Effective Java ( 이펙티브 자바 ) - 아이템 61

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


  자바의 데이터 타입은 기본 타입과 참조 타입으로 나뉜다. 그리고 각각의 기본 타입에 대응하는 참조 타입이 하나씩 있으며, 이를 박싱된 기본 타입이라고 한다. 

 

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

 

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

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

 

다음은 Integer 값을 오름차순으로 정렬하는 비교자다. Integer는 그 자체로 순서가 있으니 실질적인 의미는 없지만, 아주 흥미로운 점을 하나 보여준다.

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

별다른 문제를 찾기 어렵고, 실제로 이것저것 테스트해봐도 잘 통과한다. 하지만 심각한 결함이 숨어 있다. 

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

0을 출력해야 하지만, 실제로는 1을 출력한다. 즉, 첫번째 Integer가 두 번째보다 크다고 주장한다.

원인은 두 번째 검사인 i == j 에 있다. 이때 '객체 참조'의 식별성을 검사하게 된다. 이처럼 (같은 객체를 비교하는 게 아니라면) 박싱된 기본 타입에 == 연산자를 사용하면 오류가 일어난다.

 

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

 

그렇더라도 이 문제를 고치려면 지역변수 2개를 두어 각각 박싱된 값을 기본 타입 정수로 저장한 다음, 이 기본 타입 변수로 수행해야 한다.

Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
   int i = iBoxed, j = jBoxed; // 오토박싱
   return i < j ? -1 : ( i==j ? 0 : 1);
}
public class Unbelievable {
   static Integer i;
   
   public static void main(String[] args) {
      if (i == 42)
         System.out.println("믿을 수 없군!");
   }
}

위의 프로그램은 i == 42를 검사할 때 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을 박싱된 기본 타입으로 선언하여 느려졌다. 오류나 경고 없이 컴파일되지만, 박싱과 언박싱이 반복해서 일어나 체감될 정도로 성능이 느려진다.

 

이번 아이템에서 다룬 세 프로그램 모두 문제의 원인은 하나다. 기본 타입과 박싱된 기본 타입의 차이를 무시한 대가를 치른 것이다.

 

그렇다면 박싱된 기본 타입은 언제 써야 하는가?

  • 컬렉션의 원소, 키, 값으로 쓴다. 기본 타입을 담을 수 없다. 

더 일반화해 말하면, 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 써야 한다. 

  • 리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야 한다 (아이템65)
  가능하면 기본 타입을 사용하라. 간단하고 빠르다. 
  두 박싱된 기본 타입을 == 연산자로 비교한다면 식별성 비교가 이뤄지는데, 이는 여러분이 원한 게 아닐 가능성이 크다. 
  언박싱 과정에서 NullPointerException을 던질 수 있다.
  기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 나을 수 있다.