이번 글은 Java를 오래 사용한 개발자도 한 번쯤은 실수하는 주제다.
특히 면접에서도 자주 나오는 질문이다.
Integer a = 100;
Integer b = 100;
System.out.println(a == b);
왜 true가 나오는지,
반대로
Integer a = 1000;
Integer b = 1000;
System.out.println(a == b);
는 왜 false가 나오는지 이해하려면 반드시 알아야 하는 개념이다.
하지만 그 전에 먼저 AutoBoxing / Unboxing을 이해해야 한다.
AutoBoxing / Unboxing
primitive 타입과 Wrapper 객체를 컴파일러가 자동으로 변환해 주는 기능
(Java 5부터 지원)
먼저 primitive와 Wrapper를 구분하자.
Java에는 두 종류의 타입이 있다.
primitive 타입
byte
short
int
long
float
double
char
boolean
메모리에 값 자체를 저장한다.
Wrapper 클래스
Byte
Short
Integer
Long
Float
Double
Character
Boolean
primitive를 객체(Object)로 감싼 클래스이다.
왜 Wrapper가 필요할까?
다음 코드는 가능할까?
List<int> list = new ArrayList<>();
불가능하다.
컴파일 오류가 발생한다.
왜냐하면
Collection은
class ArrayList<E>
처럼 Generic으로 만들어져 있기 때문이다.
Generic의 타입은
Object
를 상속하는 타입만 사용할 수 있다.
하지만
int
는 객체가 아니다.
그래서
int
↓
Integer(Object)
↓
List<Integer>
를 사용하는 것이다.
Java 1.4였다면
Java 1.4에서는
직접 변환해야 했다.
Integer number =
Integer.valueOf(10);
int value =
number.intValue();
매우 번거롭다.
Java 5부터
컴파일러가 자동으로 변환해 준다.
Integer number = 10;
우리는 이렇게 작성하지만,
컴파일러는 내부적으로
Integer number =
Integer.valueOf(10);
로 바꾼다.
이것이 AutoBoxing이다.
AutoBoxing
primitive
↓
Wrapper
자동 변환
예제
Integer number = 10;
컴파일러가
Integer number =
Integer.valueOf(10);
로 변경한다.
또 다른 예제
List<Integer> list =
new ArrayList<>();
list.add(10);
우리는 int를 넣었다.
컴파일러는
list.add(
Integer.valueOf(10)
);
으로 변경한다.
즉,
int
↓
Integer
↓
List 저장
이다.
Unboxing
반대 과정이다.
Integer number = 100;
int value = number;
우리는 이렇게 작성하지만,
컴파일러는
int value =
number.intValue();
로 변경한다.
즉,
Integer
↓
int
자동 변환
이다.
연산도 가능하다.
Integer a = 10;
Integer b = 20;
int result = a + b;
우리는 객체끼리 더했다고 생각한다.
실제로는
컴파일러가
int result =
a.intValue()
+
b.intValue();
로 바꾼다.
비교도 마찬가지이다.
Integer a = 100;
if (a == 100) {
}
컴파일러는
if (a.intValue() == 100) {
}
로 변경한다.
그래서
primitive 비교
가 수행된다.
여기서 가장 위험한 부분
다음 코드는 안전할까?
Integer number = null;
int value = number;
컴파일러는
int value =
number.intValue();
로 변경한다.
실행하면
NullPointerException
이 발생한다.
왜 NPE가 발생할까?
메모리를 보면
Integer number
↓
null
이다.
그런데
number.intValue()
를 호출한다.
즉,
null.intValue()
와 같다.
그래서
NullPointerException
이 발생한다.
실무에서 자주 발생하는 예
Map<String, Integer> map =
new HashMap<>();
int count =
map.get("A");
만약
"A"
가 없다면
map.get("A")
는
null
을 반환한다.
컴파일러는
int count =
map.get("A").intValue();
처럼 처리한다.
결과
NullPointerException
그래서 실무에서는
Integer count =
map.get("A");
if (count != null) {
}
또는
int count =
map.getOrDefault("A", 0);
를 많이 사용한다.
성능도 생각해야 한다.
primitive
int value = 10;
메모리
Stack
10
Wrapper
Integer value = 10;
메모리
Stack
reference
↓
Heap
Integer 객체
↓
10
객체가 하나 더 생성된다.
대량 반복에서는
for (int i = 0; i < 1000000; i++) {
}
가
for (Integer i = 0; i < 1000000; i++) {
}
보다 훨씬 효율적이다.
Stream에서도 자주 보인다.
List<Integer> list =
List.of(1, 2, 3);
int sum =
list.stream()
.mapToInt(Integer::intValue)
.sum();
왜
mapToInt()
를 사용할까?
Stream<Integer>
↓
IntStream
↓
primitive int 사용
↓
Boxing 제거
↓
성능 향상
때문이다.
AutoBoxing은 컴파일러 기능이다.
중요한 점은
JVM이 자동 변환하는 것이 아니다.
컴파일 단계에서
Integer number = 10;
를
Integer number =
Integer.valueOf(10);
로 바꾼다.
즉,
작성 코드
↓
컴파일러 변환
↓
바이트코드
↓
JVM 실행
순서이다.
그림으로 정리
AutoBoxing
int
↓
컴파일러
↓
Integer.valueOf()
↓
Integer 객체
Unboxing
Integer
↓
컴파일러
↓
intValue()
↓
primitive int
면접 포인트
AutoBoxing이란?
primitive 타입을 Wrapper 객체로 컴파일러가 자동 변환하는 기능이다.
Unboxing이란?
Wrapper 객체를 primitive 타입으로 컴파일러가 자동 변환하는 기능이다.
가장 주의해야 할 점은?
Integer number = null;
int value = number;
은 내부적으로
number.intValue();
가 호출되므로 NullPointerException이 발생한다.
성능 관점에서는?
- 반복문, 계산 로직에서는 primitive를 우선 사용한다.
- Collection, Generic에서는 Wrapper를 사용한다.
- Stream에서는 mapToInt(), IntStream 등을 활용해 불필요한 Boxing/Unboxing을 줄이는 것이 좋다.
다음 글 추천
다음은 Integer Cache가 가장 자연스럽다.
이 글을 보면
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Integer c = 1000;
Integer d = 1000;
System.out.println(c == d); // false
가 왜 발생하는지 JVM 내부 동작까지 이해할 수 있고, ==와 equals()의 차이도 완전히 정리할 수 있다.
'language > java' 카테고리의 다른 글
| StringBuilder vs StringBuffer (0) | 2026.06.16 |
|---|---|
| Integer Cache (0) | 2026.06.16 |
| Serialization / Deserialization (0) | 2026.06.16 |
| JDK Proxy vs CGLIB (0) | 2026.06.16 |
| Dynamic Proxy 원리 (0) | 2026.06.16 |
댓글