자바 언어·플랫폼 용어 사전
언어 기초autoboxing · 언박싱 · Integer 캐시

오토박싱

원시 타입과 래퍼를 자동 변환하는 기능. 캐시 범위와 null 언박싱이라는 두 함정이 숨어 있다.

intInteger를 컴파일러가 자동으로 변환해 주는 기능.

List<Integer> list = new ArrayList<>();
list.add(1);             // 컴파일러가 Integer.valueOf(1) 로 바꾼다 (박싱)
int x = list.get(0);     // x = list.get(0).intValue()      (언박싱)

함정 ① == 비교가 값에 따라 달라진다

Integer a = 127, b = 127;
a == b;      // true

Integer c = 128, d = 128;
c == d;      // false

Integer.valueOf() 는 -128 ~ 127 을 미리 만들어 캐시해 둔다

  • 그 범위는 같은 객체를 돌려준다 → == 가 true
  • 벗어나면 new Integer 라 다른 객체 → == 가 false

같은 코드가 값에 따라 다르게 동작한다 — 테스트에서 통과하고 운영에서 터진다

래퍼 비교는 항상 equals 를 쓴다. Long·Short·Byte·Character도 같은 캐시 정책이다.

함정 ② null 언박싱

Map<String, Integer> map = new HashMap<>();
int count = map.get("없는키");    // NullPointerException
// map.get 이 null 을 반환 → null.intValue() 를 시도한다
int count = map.getOrDefault("없는키", 0);          // 안전
Integer v = map.get("key"); if (v != null) { ... }  // 또는 명시적 확인

스택 트레이스에 .intValue()가 안 보여서 원인을 찾기 어려운 유형이다.

함정 ③ 조용한 성능 손실

Long sum = 0L;                       // 래퍼로 선언했다
for (long i = 0; i < 10_000_000; i++) sum += i;
// sum = Long.valueOf(sum.longValue() + i)
// → 반복마다 Long 객체가 하나씩 생긴다 → GC 폭증
long sum = 0L;                       // 원시 타입이면 객체가 생기지 않는다

스트림에서도 같다 — IntStream·LongStreamStream<Integer>보다 훨씬 빠른 이유다.

IntStream.range(0, n).sum();                  // 박싱 없음
Stream.iterate(0, i -> i+1).limit(n)...       // 매 원소가 Integer 객체

삼항 연산자의 숨은 언박싱

Integer x = null;
Integer y = flag ? 1 : x;    // 두 피연산자 타입이 다르면 int 로 승격 → x 언박싱 → NPE

면접 함정

  • "래퍼도 == 로 비교하면 된다" → 127 이하에서만 우연히 맞는다.
  • "오토박싱은 공짜" → 객체 생성과 GC 비용이 든다.

어디서 몰래 일어나는지 찾기

Map<Integer, String> map = new HashMap<>();
map.put(1, "a");                     // Integer.valueOf(1)

List<Long> ids = ...;
if (ids.contains(1L)) { }            // Long.valueOf(1L)
if (ids.contains(1)) { }             // ⚠️ Integer 를 넣었다 → 절대 못 찾는다 (컴파일은 통과)

long sum = list.stream().mapToLong(Long::longValue).sum();   // 명시적으로 원시로

contains·remove가 타입이 달라 조용히 실패하는 것이 실무에서 가장 자주 겪는 형태다. List<Long>remove(1)을 부르면 인덱스 1번을 지운다는 더 고약한 경우도 있다.

List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
list.remove(1);                      // 인덱스 1 → 20 이 지워진다
list.remove(Integer.valueOf(1));     // 값 1 을 지운다 (없으면 아무 일도 없음)

컴파일 결과 확인

javap -c MyClass.class | grep valueOf
# invokestatic java/lang/Integer.valueOf   ← 박싱이 일어난 지점

핫 루프에서 이게 보이면 원시 타입이나 원시 특화 스트림으로 바꿀 신호다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 기초·타입·객체지향