⚓️서론
자바의 데이터 타입은 크게 두 가지로 나눌 수 있다.
기본타입(Primitive Type)과 참조타입(Reference Type)이 있다.
기본타입에는 각각 대응하는 참조타입이 있는데, 그것을 박싱된 기본타입이라고 한다.
기본 타입 | 래퍼(박싱) 타입 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
➡️오토 박싱(AutoBoxing)과 오토 언박싱(AutoUnBoxing)
// Integer -> int
Integer num = new Integer(10);
int i = num; // Auto UnBoxing
//int -> Integer
int num2 = 10;
Integer j = 10; // Auto Boxing
JDK 1.5부터는 박싱과 언박싱이 필요한 상황에서 자바 컴파일러가 이를 자동으로 처리해준다.
그전에는 시대는.. 참 귀찮았다.
- intValue()메소드를 사용해서 UnBoxing
Integer num = new Integer(10); // Boxing
int n = num.intValue(); // Unboxing
- Integer.valueOf() 메소드를 사용해서 Boxing
int num2 = 10;
Integer j = Integer.valueOf(num2); // Boxing
그렇다면 기본 타입과 래퍼타입은 무슨 차이일까?
➡️기본타입(Primitive Type) VS 박싱된 기본타입(Wrapper Type)
- 두 개의 차이점은 크게 세 가지다.
1️⃣ 기본 타입은 값만 가지고 있다. 반면에 박싱된 기본타입은 값 + 식별성 속성
을 갖고 있다.
2️⃣ 기본 타입의 값은 언제나 유효하지만 박싱된 기본 타입은 유효하지 않는 값, Null을 가질 수 있다.
3️⃣ 기본 타입이 박싱된 기본 타입보다 시간과 메모리 사용면에서 더 효율적이다.
📎참고 - Java Primitives versus Objects
위의 세 가지를 유의하지 않고 사용할 경우 문제가 발생할 수 있다.
➡️ 문제가 발생하는 경우
1️⃣잘못 구현된 비교자 예시
public class BrokenComparator {
public static void main(String[] args) {
Comparator<Integer> naturalOrder =
(i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
int result = naturalOrder.compare(new Integer(42), new Integer(42));
System.out.println(result); // 1
}
}
두 Integer 인스턴스의 값이 42로 같으므로 0을 출력해야 하지만, 실제로는 1을 출력한다.
❓출력은 1 이다. 왜? 0이 아니고 1일까?
🔡원인
- 첫번째 연산(i< j)에서 i와 j는 참조하는 오토박싱된 Integer 인스턴스는 기본 타입 값으로 변환된다.
Integer → int
: UnBoxing - 첫번째 연산 → false , 두번째 연산(i == j) → false 발생한다.
🔡분석
- (i,j)의 타입이 Integer로 추론되기 떄문에 두번째 연산(i == j)이 진행될 때 동일성 검사가 진행되어 false가 발생한다.
- i와 j는 값은 같지만 서로 다른 객체이다.
⚠️따라서 박싱된 기본 타입에 ==
연산자를 사용하면 다른 결과가 나올 수 있다.
위와 같은 문제를 고치려면..
지역변수 2개를 두어 각각 박싱된 Integer 매개변수의 값을 기본 타입 정수로 저장한 다음 모든 비교를 이 기본 타입 변수로 수행하면 된다. 이렇게 하면 오류의 원인인 식별성 검사
는 이뤄지지 않는다.
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
int i = iBoxed, j = jBoxed; // 오토박싱
return i < j ? -1 : (i == j ? 0 : 1);
};
2️⃣ NullPointerException 발생
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if (i == 42) System.out.println("믿을 수 없군!");
}
}
위 프로그램은 '믿을 수 없군!’
을 출력하지 못하고 NullPointerException
을 던진다.
❓‘믿을 수 없군!’이 아니라 왜 NullpointerException을 던지는 것일까?
🔡원인
- (i == 42)를 검사하는 과정에서 NullPointerException을 던진다.
- i가 리터럴 값인 42와 비교하는 과정에서 i는 Auto UnBoxing을 수행, 하지만 i가 null이기 떄문에 Auto Unboxing 과정 중에 NullPointerExceotion을 던진다.
기본 타입과 박싱된 기본 타입을 혼용한 연산에서는 박싱된 기본 타입의 박싱이 자동으로 풀린다.
위의 해법은 i를 int로 선언해주면 된다. 너무 간단하다..
3️⃣ Auto Boxing과 Auto UnBoxing으로 인한 성능 저하
public class App {
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은 박싱된 기본타입(Long)으로 선언되어 있다.
- sum += i; 이 진행되는 코드에서 sum이 long타입으로 Auto UnBoxing이 되고 sum + i 연산이 진행되고 Long타입으로 Auto Boxing된다.
- 이렇게 반복을 하다보면 매우 성능이 느려진다.
➡️박싱된 기본 타입은 언제 써야 하는가?
1️⃣ 컬렉션의 원소, 키 , 값으로 쓴다.
컬렉션은 기본 타입을 담을 수 없으므로 어쩔 수 없이 박싱된 기본 타입을 써야만 한다.
제네릭(Generics) 타입을 이용하는 경우에도 박싱된 기본타입을 사용한다.
- 제네릭 타입에서는 int, double과 같은 기본타입을 지원하지 않기 때문이다.
• 리플렉션(Reflection)을 통해 메서드를 호출할 때에도 박싱된 기본타입을 사용한다.
✅핵심 정리
오토 박싱이 박싱된 기본 타입을 사용할 때의 번거로움을 줄여주지만, 위와 같이 세 가지의 위험은 없애주지 않는다.
- 두 박싱된 기본 타입을 ==연산자로 비교한다면 식별성 비교가 이뤄지므로 예상했던 결과와 다르게 나온다.
- 같은 연산에서 기본타입과 박싱된 기본 타입을 혼용하면 언박싱이 이뤄지며, 언박싱 과정에서 NullPoinerException을 던질 수 있다.
- 기본 타입을 박싱하는 작업은 필요 없는 객체를 생성하는 부작용을 초래할 수 있다.