Java/Effective Java

[Effective Java] 아이템 61. 박싱된 기본 타입보다는 기본 타입을 사용해라

메성 2020. 2. 3. 08:48
반응형

Item 61. 박싱된 기본 타입보다는 기본 타입을 사용해라

JDK 1.5버전에서는 오토박싱과 오토언박싱 덕분에 두 타입을 크게 구분하지 않고 사용할 수 있다. 하지만 두 개의 차이는 명확하게 구분된다.

 

 

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

  1. 기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값 + 식별성이라는 속성을 갖는다.

    • 다시 말하면, 값이 같은 박싱된 기본 타입의 인스턴스가 두 개 존재할 때, 이 두 개는 서로 다르다고 식별될 수 있다.
  2. 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않은 값을 가질 수 있다. 즉, null을 가질 수 있다.

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

세 가지 차이로 인해 주의하지 않고 사용하면 문제가 발생할 수 있다.

 

 

이제 각 차이를 소스와 함께 살펴보자


1. 박싱된 기본 타입은 값 + 식별성이라는 속성을 갖는다.

public static void main(String [] args) {
  Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
  int num = naturalOrder.compare(new Integer(42), new Integer(42));
  System.out.println(num);
}
//실행 결과 : 1

들어가기에 앞서 Compare 메소드를 다시 한번 살펴보면, i값과 j값을 비교할 때(compare(i, j))

  • i < j : -1
  • i > j : 1
  • i == j : 0

그런데 결과를 살펴보면 1 이 나타나는 것을 확인할 수 있다. 왜? 같아야 하는거 아닌가?!

 

 

원인을 살펴보자

  • 처음 (i < j) 의 검사는 잘 작동한다. 여기에서 i와 j가 참조하는 오토박싱된 Integer 인스턴스는 기본 타입으로 변환된다
  • 즉, 첫번째 정수값(i)이 두번째 정수값(j)보다 작은지 확인한다.
  • 그리고 첫번째 정수값(i)이 두번째 정수값(j)보다 작지 않으면 두번째 검사인 (i == j)가 이루어진다. 그런데 이 두번째 검사에서는 두 객체 참조의 식별성 검사를 하게 된다.
    • 즉, i와 j가 서로 다른 Integer 인스턴스라면(참조 주소가 다른..즉 값이 같아도 주소는 다르다.) 비교의 결과는 false가 되고 비교자는 1을 반환하게 되는 것이다.

이 처럼 박싱된 기본 타입에 == 연산자를 사용하면 오류가 발생하는 것을 볼 수 있다. why? 식별성이라는 속성을 가지고 있으니깐!

 

 

문제를 해결해보자

public static void main(String [] args) {
  Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
    int i = iBoxed, j = jBoxed; //오토 언박싱
    return i < j ? -1 : (i == j ? 0 : 1);
  };
  int result = naturalOrder.compare(new Integer(43), new Integer(43));
  System.out.println(result);
}
  • 지역 변수 2개를 두고 각각의 Integer 매개변수의 값을 기본 타입의 정수로 오토 언박싱하여 저장한다.
    • 이로 인해서 모든 비교를 저장한 기본타입으로 진행된다.

결과적으로, 기본 타입으로 박싱된 값을 다른 기본 타입에 저장하고, 그 값으로 비교를 진행한 것이다.

이로인해, 식별성이라는 속성은 없어지고 우리가 원하는 비교 연산을 진행할 수 있는 것이다.

 

 

2. 박싱된 기본 타입(int가 Integer로 박싱)은 null을 가질 수 없다.

public class BoxingNull {
  static Integer i;

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

//실행 결과 : NullPointerException

위 소스의 결과를 확인하면 NullPointException이라는 결과가 나타나는 것을 확인할 수 있다.

원인은 i가 int(기본타입)이 아닌 Integer(참조타입)이기 때문이다.

  • 즉, i == 43은 Integer와 int를 비교하는 것이다.
  1. 거의 예외없이 기본 타입과 박싱된 기본타입(int가 Integer로 박싱)을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀린다.
  2. 다시 말해, 기본 타입으로 다시 돌아간다는 얘기다.
  3. 그로 인해 null 참조를 언박싱(기본 타입으로 돌아감)하면 NullPointerException이 발생한다.
  • 쉽게 말해 i 값이 Integer에서 int로 변환되는 것이다.

다행히 해결방법은 i를 int로 선언해주면 된다.

 

 

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

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

위 소스는 지역변수 sum을 박싱된 기본타입인 Long으로 선언하여 오류없이 컴파일은 되지만, 박싱과 언박싱으로 반복해서 일어나므로 성능상 매우 느려진다.

 

그러면 우리는 박싱된 기본타입은 언제 써야할까?

  1. 컬렉션의 원소, 키, 값으로 쓴다.
    • 컬렉션은 기본 타입으로 담을 수 없으므로 어쩔수 없이 박싱된 기본타입을 사용해야 한다.
    • 일반화해서 말하면 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 사용해야 한다.
      • Ex. ThreadLocal<>
      • Ex. method(ThreadLocal<>)
  2. 리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야한다.

 

정리

기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 되도록이면 기본 타입을 사용해라.

  1. 두 박싱된 기본 타입을 == 연산자로 비교한다면 식별성 비교가 이뤄지므로 우리가 원하는 결과가 나타나지 않을 확률이 높다.
  2. 같은 연산에서 언박싱과 박싱된 기본타입을 혼용해서 사용하면 박싱된 기본 타입이 언박싱이 이뤄지면 언박싱 과정에서 NullPointerException 던질 수가 있다.
  3. 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 나을 수 있다.
반응형