Item61. 박싱된 기본 타입보다는 기본 타입을 사용하라
자바의 데이터 타입은 2가지로 나뉘는데
기본 타입인 byte, short, int, long, float, double, boolean, char과
그외에는 모두 참조형 타입의 데이터이다.
각각의 기본 타입에 대응하는 참조 타입이 하나씩 있는데 이를 박싱된 기본 타입이라고 한다.
기본 타입과 박싱된 기본 타입의 차이점
- 기본 타입은 값만 가지고 있으나 박싱된 기본 타입은 값에 더해 식별성이란 속성을 갖는다.
달리 말하면 박싱된 기본 타입의 두 인스턴스는 값이 같아도 서로 다르다고 식별될 수 있다. - 기본 타입의 값은 언제나 유효하나, 박싱된 기본 타입은 유효하지 않은 값, 즉 null을 가질 수 있다.
- 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
박싱된 기본 타입의 문제점
Integer 값을 오름차순으로 정렬하는 비교자를 봤을 때 두 Integer의 값이 42로 같으므로 0을 출력해야하지만 1을 출력한다.
원인은 naturalOrder의 첫 번째 검사 ( i < j )는 잘 동작한다. 여기서 i와 j가 참조하는 오토박싱된 Integer 인스턴스는 기본 타입 값으로 변환된다. 다음 첫 번째 정수값이 두 번째 값보다 작은지 평가한다. 만약 작지 않다면 두번쨰 검사 ( i == j )가 이뤄진다. 그런데 이 두 번째 검사에서 두 객체 참조의 식별성을 검사하게 된다. i와 j가 서로 다른 Integer 인스턴스라면 비록 값은 같더라도 이 비교의 결과는 false가 되고 비교자는 1을 반환한다.
이처럼 박싱된 기본 타입에 == 연산자를 사용하면 오류가 난다.
이 문제를 고치려면 지역변수 2개를 두어 각각 박싱된 Integer 매개변수의 값을 기본 타입 정수로 언박싱하여 저장한 다음 모든 비교를 이 기본타입 변수로 수행해야 하면 오류의 원인인 식별성 검사가 이루어지지 않는다.
이 프로그램은 "믿을 수 없군"을 출력하지 않지만 i == 42 를 검사할 때 NullPointerException을 던진다. 원인은 i가 int 가 아닌 Integer이며 참조타입의 초기값이 null 이라는데 있다.
즉 i == 42는 Integer와 int를 비교하는 것이다. 거의 예외 없이 기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀린다. 그리고 null 참조를 언박싱하면 NullPointerException이 발생한다. 해법은 i를 int로 선언해주면 된다.
이 프로그램은 실수로 지역변수 sum을 박싱된 기본 타임으로 선언하여 느려졌다. 오류나 경고 없이 컴파일 되지만 박싱과 언박싱이 반복해서 일어나 체감될 정도로 성능이 느려진다.
이번 아이템에서 다룬 세 프로그램 모두 문제의 원인은 프로그래머가 기본 타입과 박싱된 기본 타입의 차이를 무시한 대가를 치른것이다.
박싱된 기본 타입을 써야할 때
- 컬렉션의 원소, 키, 값으로 쓴다. 컬렉션은 기본 타입을 담을 수 없으므로 어쩔 수 없이 박싱된 기본타입을 써야만 한다.
- 매개변수화 타입이나 매개변수화 메서드의 타입 매개변수로는 박싱된 기본 타입을 써야 한다. 자바 언어가 타입 매개변수로 기본 타입을 지원하지 않기 때문이다.
- 리플렉션을 통해 메서드를 호출할 때도 박싱된 기본 타입을 사용해야 한다.
핵심 정리
기본 타입과 박싱된 기본 타입 중 하나를 선택해야 한다면 가능하면 기본 타입을 사용하라. 기본타입은 간단하고 빠르다. 박싱된 기본 타입을 써야 한다면 주의를 기울이자.
오토박싱이 박싱된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 그 위험까지 없애주지는 않는다.
두 박싱된 기본 타입을 == 연산자로 비교한다면 식별성 비교가 이뤄지는데 이는 프로그래머가 원한게 아닐 가능성이 크다.
같은 연산에서 기본 타입과 박싱된 기본 타입을 혼용하면 언박싱이 이뤄지며, 언박싱 과정에서 NullPointerException을 던질 수 있다. 마지막으로, 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 나을 수 있다.