[Effective Java] 아이템 61. 박싱된 기본 타입보다는 기본 타입을 사용해라
Item 61. 박싱된 기본 타입보다는 기본 타입을 사용해라
JDK 1.5버전에서는 오토박싱과 오토언박싱 덕분에 두 타입을 크게 구분하지 않고 사용할 수 있다. 하지만 두 개의 차이는 명확하게 구분된다.
기본 타입과 박싱된 기본 타입의 차이
-
기본 타입은 값만 가지고 있으나, 박싱된 기본 타입은 값 + 식별성이라는 속성을 갖는다.
- 다시 말하면, 값이 같은 박싱된 기본 타입의 인스턴스가 두 개 존재할 때, 이 두 개는 서로 다르다고 식별될 수 있다.
-
기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않은 값을 가질 수 있다. 즉, null을 가질 수 있다.
-
기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
이 세 가지 차이로 인해 주의하지 않고 사용하면 문제가 발생할 수 있다.
이제 각 차이를 소스와 함께 살펴보자
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를 비교하는 것이다.
- 거의 예외없이 기본 타입과 박싱된 기본타입(int가 Integer로 박싱)을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀린다.
- 다시 말해, 기본 타입으로 다시 돌아간다는 얘기다.
- 그로 인해 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으로 선언하여 오류없이 컴파일은 되지만, 박싱과 언박싱으로 반복해서 일어나므로 성능상 매우 느려진다.
그러면 우리는 박싱된 기본타입은 언제 써야할까?
- 컬렉션의 원소, 키, 값으로 쓴다.
- 컬렉션은 기본 타입으로 담을 수 없으므로 어쩔수 없이 박싱된 기본타입을 사용해야 한다.
- 일반화해서 말하면 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 사용해야 한다.
- Ex. ThreadLocal<>
- Ex. method(ThreadLocal<>)
- 리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야한다.
정리
기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 되도록이면 기본 타입을 사용해라.
- 두 박싱된 기본 타입을 == 연산자로 비교한다면 식별성 비교가 이뤄지므로 우리가 원하는 결과가 나타나지 않을 확률이 높다.
- 같은 연산에서 언박싱과 박싱된 기본타입을 혼용해서 사용하면 박싱된 기본 타입이 언박싱이 이뤄지면 언박싱 과정에서 NullPointerException 던질 수가 있다.
- 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 나을 수 있다.