[JAVA] Queue는 왜 안되고 Queue는 왜 될까?
“왜 Queue
왜 Queue<int>
는 안 되고 Queue<Integer>
는 될까?
Java 제네릭을 쓰다 보면 한 번씩 멈칫하게 되는 질문입니다. “왜 Queue<int>
는 안 되는데 Queue<Integer>
는 되지?” 이 글은 그 이유를 **타입 소거(type erasure)**와 참조 타입만 허용하는 제네릭 규칙에서 차근차근 풀어 설명합니다. 실전 성능 팁과 BFS 같은 알고리즘 코드 패턴도 함께 담았습니다.
정리
- 제네릭 타입 인자(T)는 참조 타입만 가능합니다. (
T extends Object
가 암묵적 전제) - 타입 소거로 인해 런타임에는 제네릭 정보가 지워지고, 메서드 시그니처가 사실상
Object
기반으로 동작합니다. int
같은 원시 타입(primitive) 은Object
가 아니므로 제네릭 인자로 쓸 수 없습니다. ⇒Queue<int>
금지- 배열은 참조 타입이므로
Queue<int[]>
는 가능합니다. (배열 자체는 객체) - 알고리즘 큐에는
int[]
또는record/class
로 상태를 묶어 넣으면 박싱 없이 빠르고 메모리 친화적입니다.
1) 제네릭은 왜 쓰나?
컴파일 타임에 타입을 체크해 타입 안전성을 높이고, 캐스트를 없애 가독성과 유지보수성을 올리기 위해서입니다. 제네릭에 대해 정리한 블로그 글 참고
// 제네릭 없음: 캐스트 필요
List list = new ArrayList();
list.add("hi");
String x = (String) list.get(0);
// 제네릭 사용: 컴파일 타임에 체크, 캐스트 제거
List<String> list2 = new ArrayList<>();
list2.add("hi");
String y = list2.get(0);
2) 타입 소거(type erasure)란?
자바의 제네릭은 런타임에 사라집니다. 컴파일러가 제네릭 코드를 검사·보정한 뒤, 실행 시점에는 타입 매개변수를 지운(Object로 대체한) 형태로 동작합니다.
개념적으로 다음과 같습니다.
// 원본
class Box<T> {
void put(T x) { /* ... */ }
T get() { /* ... */ }
}
// (개념적) 컴파일 후 - T가 지워지고 Object 중심으로
class Box {
void put(Object x) { /* ... */ }
Object get() { /* ... */ }
}
컴파일러가 캐스트 삽입과 오토박싱/언박싱으로 타입 안전을 보정해 줍니다(필요 시 브리지 메서드도 생성).
3) T는 왜 ‘참조 타입’만 될 수 있나?
자바 언어 규칙상 **모든 타입 매개변수는 암묵적으로 T extends Object
**로 취급됩니다.
Integer
,String
,MyClass
같은 참조 타입(reference type) 은Object
의 하위 타입이므로 OK.int
,double
,boolean
같은 원시 타입(primitive) 은Object
가 아니므로 제네릭 타입 인자로 금지됩니다.
즉, Queue<int>
는 언어 차원에서 성립하지 않습니다.
포인트: 타입 소거 후의 세계는
Object
중심이라, 그 세계로 들어올 수 있는 타입(=참조 타입)만 제네릭 인자가 될 수 있습니다.
4) 그렇다면 Queue<Integer>
는 왜 되나?
Integer
는 int
의 래퍼 클래스(참조 타입)입니다. 제네릭 인자로 쓸 수 있고, q.offer(1)
처럼 쓰면 오토박싱이 자동으로 일어나 int -> Integer
가 됩니다.
Queue<Integer> q = new ArrayDeque<>();
q.offer(1); // int가 Integer로 오토박싱
int v = q.poll(); // Integer가 int로 언박싱
단, 이 과정은 객체 할당/GC 비용이 들 수 있습니다. 대량 연산에서는 체감될 수 있어요.
5) Queue<int[]>
는 왜 가능한가?
배열은 항상 참조 타입입니다. 원소가 원시 타입이든 말든, 배열 자체는 힙 객체니까 제네릭 인자로 사용 가능해요.
Queue<int[]> q = new ArrayDeque<>();
q.offer(new int[]{1,2,3}); // 배열 참조를 넣음
int[] arr = q.poll();
6) 성능·메모리 관점: 오토박싱을 피하자
Queue<Integer>
는 원소마다Integer
객체가 생길 수 있어- 오토박싱/언박싱 비용
- 객체 헤더 + 포인터 오버헤드
- GC 부담 이 발생합니다.
알고리즘(특히 BFS/DFS, 다익스트라 등)에서는 박싱을 피하는 게 유리합니다.
권장 1) int[]
로 상태 묶기 (가볍고 빠름)
// {r, c, breakUsed, dist}
ArrayDeque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{0, 0, 0, 1});
int[] cur = q.poll();
int r = cur[0];
권장 2) record
로 가독성 ↑ (Java 16+)
record State(int r, int c, int b, int d) {}
ArrayDeque<State> q = new ArrayDeque<>();
q.offer(new State(0, 0, 0, 1));
State s = q.poll();
팁: 큐 구현체는 **
ArrayDeque
**가 일반적으로LinkedList
보다 빠르고 메모리 효율적입니다.
7) 실전 FAQ
Q1. “런타임에 진짜 Queue<Object>
로 동작하나요?” 개념적으로는 그와 유사합니다(타입 소거). 실제로는 컴파일러가 캐스트/브리지 메서드 등으로 타입 안전을 맞춰 줍니다. 핵심은 런타임에 타입 인자 정보가 없고 Object
중심으로 호출된다는 점입니다.
Q2. 그렇다면 왜 컴파일러가 Queue<int>
도 자동으로 Queue<Integer>
로 바꿔주지 않나요? 언어 규칙상 제네릭 인자는 참조 타입만 허용합니다. 타입 인자 자체를 바꾸는 묵시적 변환은 설계상 모호성과 함정(예: T
가 원시로 선언됐는데 실제론 참조로 다뤄짐)을 낳기에 금지됩니다. 명시적으로 Integer
를 써 주세요.
Q3. 원시 타입 컬렉션이 꼭 필요합니다. 방법이 없나요? 표준 라이브러리는 제공하지 않지만, 전용 라이브러리가 있습니다.
- fastutil (
IntArrayList
,IntOpenHashSet
, …) - HPPC (High Performance Primitive Collections)
- Eclipse Collections (primitive collections) 대량 데이터·고성능 시나리오에서 유용합니다.
8) BFS 예시: 박싱 없이 깔끔하게
아래는 “벽을 한 번만 부술 수 있는” BFS 패턴입니다. int[]
로 상태를 묶어 박싱을 회피합니다.
import java.io.*;
import java.util.*;
public class Main {
static final int[] dr = {1, -1, 0, 0};
static final int[] dc = {0, 0, 1, -1};
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int N = Integer.parseInt(st.nextToken());
int M = Integer.parseInt(st.nextToken());
int[][] map = new int[N][M];
for (int i = 0; i < N; i++) {
String line = br.readLine();
for (int j = 0; j < M; j++) map[i][j] = line.charAt(j) - '0';
}
boolean[][][] visited = new boolean[N][M][2];
ArrayDeque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{0, 0, 0, 1}); // r, c, breakUsed, dist
visited[0][0][0] = true;
int ans = -1;
while (!q.isEmpty()) {
int[] cur = q.poll();
int r = cur[0], c = cur[1], b = cur[2], d = cur[3];
if (r == N - 1 && c == M - 1) { ans = d; break; }
for (int k = 0; k < 4; k++) {
int nr = r + dr[k], nc = c + dc[k];
if (nr < 0 || nr >= N || nc < 0 || nc >= M) continue;
if (map[nr][nc] == 0) {
if (!visited[nr][nc][b]) { visited[nr][nc][b] = true; q.offer(new int[]{nr, nc, b, d + 1}); }
} else if (b == 0 && !visited[nr][nc][1]) {
visited[nr][nc][1] = true; q.offer(new int[]{nr, nc, 1, d + 1});
}
}
}
System.out.println(ans);
}
}
9) 한눈에 요약 체크리스트
- 제네릭 인자 = 참조 타입만 (암묵적
T extends Object
) - 런타임 = 타입 소거 (제네릭 정보 없음,
Object
중심) Queue<int>
❌,Queue<Integer>
⭕Queue<int[]>
⭕ (배열은 참조 타입)- 성능 중요: 박싱 피하기 (가능하면
int[]
/record
사용) - 큐 구현체는 보통
ArrayDeque
추천
마무리
Queue<int>
가 금지되는 이유는 단순히 “문법이 그렇다”가 아니라, 타입 소거라는 실행 모델과 참조 타입만 허용하는 제네릭 설계가 맞물린 결과입니다. 오늘부터는 알고리즘에서 박싱 없는 상태 표현으로 깔끔하고 빠른 코드를 써 보세요!