01. 제네릭 (Generic)
제네릭이란 클래스나 메서드의 코드를 작성할 때, 타입을 구체적으로 지정하는 것이 아니라, 추후에 지정할 수 있도록 일반화해 두는 것을 의미한다. 즉, 작성한 클래스 또는 메서드의 코드가 특정 데이터 타입에 얽매이지 않게 해 둔 것을 뜻한다.
1. 제네릭 클래스 (Generic Class)
이처럼 제네릭이 사용된 클래스를 제니릭 클래스라고 하며, <T> 형식을 통해 클래스 내부에서 사용할 타입 파라미터를 선언할 수 있다.
하지만 모든 인스턴스가 공유하는 클래스 변수에는 제네릭을 사용할 수 없다. 왜냐하면 만약 제네릭을 통해 클래스 변수의 타입을 유동적으로 변환할 수 있도록 한다면, 그는 모든 인스턴스가 공유해야 하는 클래스 변수의 타입을 다르게 만들어 궁극적으로 모든 인스턴스가 해당 클래스 변수를 공유할 수 없게 되기 때문이다.
Basket<String> basket1 = new Basket<String>("Hello");
Basket<Integer> basket2 = new Basket<Integer>(10);
Basket<Double> basket3 = new Basket<Double>(3.14);
2. 래퍼 클래스 (Wrapper Class)
또한 이러한 제네릭 클래스는 참조 타입만 사용할 수 있기 때문에, 제네릭 클래스를 인스턴스화 할 때에는 반드시 의도하고자 하는 래퍼 타입 형식을 지정해주어야 한다.
래퍼 클래스 (Wrapper Class)란 int, char, double 등과 같은 기본 타입 (Primitive Type)을 객체로 감싼 클래스를 뜻한다. 주로 기본 타입의 데이터를 객체로 변환하여 컬렉션 프레임워크에 사용하거나 해당 데이터를 null 값으로 처리하기 위해 사용한다.
| 기본 타입 (Primitive) |
래퍼 클래스 (Wrapper) |
캐싱 여부 | 캐싱 범위 |
| byte | Byte, | O | 모든 값 |
| short | Short | O | -128 ~ 127 |
| int | Integer | O | -128 ~ 127 |
| long | Long | O | -128 ~ 127 |
| char | Character | O | 0 ~ 127 (ASCII) |
| float | Float | X | 없음 |
| double | Double | X | 없음 |
| boolean | Boolean | O | true / false |
기본적으로 Java는 자주 사용되는 작은 숫자들을 미리 래퍼 클래스 객체로 만들어 두고, 필요할 때마다 새로 만드는 대신 재사용하는데 이를 캐싱이라고 한다. 즉, 특정 범위 내의 숫자를 박싱할 때, 힙(Heap)에 매번 새로운 객체를 생성하는 것이 아니라 미리 생성된 객체의 주소값을 반환한다. 다시 말해 특정 범위를 벗어나면 Java는 매번 새로운 래퍼 클래스 객체를 만들기 때문에, 특정 범위를 벗어난 객체는 반드시 등가 연산자(==)가 아닌 .equals() 함수로 비교해야 한다.
또한 기본 타입을 래퍼 클래스로 변환하는 것을 박싱(Boxing)이라고 하고, 래퍼 클래스를 기본 타입으로 변환하는 것을 언박싱(Unboxing)이라고 한다. 이는 JDK 1.5부터 컴파일러가 자동으로 수행한다. 추가로 문자열 객체를 기본 타입으로 변환하는 것은 파싱(Parsing)이라고 한다.

int value = 20;
Integer boxed = Integer.valueOf(value); // 박싱
int unboxed = boxed.intValue(); // 언박싱
int num = Integer.parseInt("123"); // 문자열 -> 기본 타입
String str = Integer.toString(123); // 기본 타입 -> 문자열
String str2 = 123 + ""; // 문자열 결합으로도 가능
2. 타입 파라미터 (Type Parameter)
타입 파라미터란 제네릭 클래스나 메서드에서 타입을 일반화하기 위해 사용하는 대체 타입 변수를 의미한다. 앞서 제네릭 클래스에서 본 것처럼, <T> 형식으로 지정하며 실제 타입은 사용 시점(런타임)에 결정된다. 관용구처럼 사용되는, 대표적인 타입 파라미터의 종류로는 아래의 표와 것들이 있다.
| 타입 파라미터 | |
| T | 타입 (Type) |
| E | 요소 (Element) |
| K | 키 (Key) |
| V | 값 (Value) |
| N | 이름 (Name) |
3. 제네릭 클래스의 종류
| 제한 없는 제네릭 (<T>) |
제한된 제네릭 (<T extends T1>) |
|
| 핵심 개념 | 어떤 타입이든 인스턴스화 가능 | 특정 클래스나 인터페이스의 하위 타입만 인스턴스화 가능 |
| 선언 방식 | public class Box<T> { ... } | public class NumberBox<T extends Number> { ... } |
| 사용 가능한 메서드 | Object 클래스의 메서드만 사용 가능 | T1(부모) 클래스에 정의된 메서드 사용 가능 |
| 타입 안정성 | 매우 유연하지만, 타입 체크가 느슨함 | 허용되지 않은 타입 사용 시 컴파일 에러 발생 |
| 활용 사례 | ArrayList<T>, Stack<T> 등 범용 바구니 | 숫자 계산용 클래스, 특정 인터페이스 구현체 전용 |
// 1. 제한 없는 제네릭 클래스
class Basket<T> {
private T item;
public T getItem() { return item; }
public void setItem(T item) { this.item = item; }
}
// 2. 제한된 제네릭 클래스 (클래스 ver)
class Flower { ... }
class Rose extends Flower { ... }
class Basket<T extends Flower> {
private T item;
public T getItem() { return item; }
public void setItem(T item) { this.item = item; }
}
// 3. 제한된 제네릭 클래스 (인터페이스 ver)
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
class Basket<T extends Plant> {
private T item;
public T getItem() { return item; }
public void setItem(T item) { this.item = item; }
}
// 4. 제한된 제네릭 클래스 (클래스 + 인터페이스 ver)
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
class Basket<T extends Flower & Plant> { // 반드시 클래스, 인터페이스 순서
private T item;
public T getItem() { return item; }
public void setItem(T item) { this.item = item; }
}
02. 와일드 카드 (Wildcard)
와일드 카드(?)란 제네릭 타입에 모호한 타입 또는 범위를 제한하고자 할 때 사용하는 문법이다. 해당 문법을 통해 제네릭 클래스나 메서드의 사용 유연성을 높일 수 있다.
1. 와일드 카드의 종류
| 와일드 카드의 종류 | 특징 | |
| <?> | 모든 클래스 타입 허용 | <? extends Object>와 동일한 개념 |
| <? extends T> | T 및 그 하위 타입 허용 >> 상한 제한 |
읽기 전용 |
| <? super T> | T 및 그 상위 타입 허용 >> 하한 제한 |
쓰기 전용 |
2. 와일드 카드 예제
class Phone {}
class IPhone extends Phone {}
class Galaxy extends Phone {}
class IPhone12Pro extends IPhone {}
class IPhoneXS extends IPhone {}
class S22 extends Galaxy {}
class ZFlip3 extends Galaxy {}
class User<T> {
public T phone;
public User(T phone) {
this.phone = phone;
}
}

| 기능 | 제한 문법 | |
| call() | 모든 폰에서 통화 가능 | ? extends Phone |
| faceId() | 아이폰만 사용 가능 | ? extends IPhone |
| samsungPay() | 삼성폰만 사용 가능 | ? extends Galaxy |
| recordVoice() | 안드로이드만 가능 | ? super Galaxy |
// 1. 모든 타입 허용
PhoneFunction.call(new User<Phone>(new Phone()));
PhoneFunction.call(new User<IPhone>(new IPhone()));
PhoneFunction.call(new User<Galaxy>(new Galaxy()));
PhoneFunction.call(new User<IPhone12Pro>(new IPhone12Pro()));
PhoneFunction.call(new User<IPhoneXS>(new IPhoneXS()));
PhoneFunction.call(new User<S22>(new S22()));
PhoneFunction.call(new User<ZFlip3>(new ZFlip3()));
// 2. 아이폰과 그 하위 타입만 허용
// PhoneFunction.faceId(new User<Phone>(new Phone()));
PhoneFunction.faceId(new User<IPhone>(new IPhone()));
PhoneFunction.faceId(new User<IPhone12Pro>(new IPhone12Pro()));
PhoneFunction.faceId(new User<IPhoneXS>(new IPhoneXS()));
// PhoneFunction.faceId(new User<Galaxy>(new Galaxy()));
// 3. 갤럭시와 그 하위 타입만 허용
// PhoneFunction.samsungPay(new User<Phone>(new Phone()));
// PhoneFunction.samsungPay(new User<IPhone>(new IPhone()));
PhoneFunction.samsungPay(new User<Galaxy>(new Galaxy()));
PhoneFunction.samsungPay(new User<S22>(new S22()));
PhoneFunction.samsungPay(new User<ZFlip3>(new ZFlip3()));
// 4. 갤럭시 이상의 상위 타입만 허용
PhoneFunction.recordVoice(new User<Phone>(new Phone()));
// PhoneFunction.recordVoice(new User<IPhone>(new IPhone()));
PhoneFunction.recordVoice(new User<Galaxy>(new Galaxy()));
// PhoneFunction.recordVoice(new User<S22>(new S22()));
3. 제한 타입 파라미터 vs 와일드 카드
| 제한 타입 파라미터 (Bounded Type Parameter) |
와일드카드 (Wildcard) |
|
| 문법 | <T extends Number> | List<? extends Number> |
| 선언 위치 | 클래스 / 메서드 정의부 | 사용부 |
| 사용 목적 | 제네릭 타입 정의 및 제약 | 타입 유연성 확보 |
| 사용 시점 | 제네릭 정의 | 제네릭 사용 |
| 타입 이름 | T, E, K, 등 명시 필요 | ? |
| 읽기 가능 여부 | 가능 | 하한 제한(<? extend T>)만 가능 |
| 쓰기 가능 여부 | 가능 | 상한 제한(<? super T>)만 가능 |
| 코드 가독성 | 코드 재사용성과 명확한 타입 사용에 유리 | 코드 유연성과 범용성에 유리 |
// 1. Bounded Type Parameter
public class Calculator<T extends Number> {
public double add(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
}
// 2. 와일드 카드
public void printAll(List<? extends Number> list) {
for (Number num : list) {
System.out.println(num);
}
}
03. 타입 안전성 보장 방법
// 1. 경고 제거
List rawList = new ArrayList(); // 경고 발생
List<String> stringList = new ArrayList<>();
// 2. 타입 매개변수 명시
List<Integer> intList = List.of(1, 2, 3);
Integer num = intList.get(0);
// 3. 경계 와일드 카드 활용
public void printNumbers(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
}
// 4. 제네릭 유릴리티 메서드
public class SafeCaster {
public static <T> T cast(Object obj, Class<T> clazz) {
if (clazz.isInstance(obj)) {
return clazz.cast(obj);
}
throw new ClassCastException("Invalid cast.");
}
}
'[Codeit Review] > 알고리즘과 자료구조' 카테고리의 다른 글
| [05] 객체 직렬화 (0) | 2026.01.30 |
|---|---|
| [04] File I/O 기초와 활용 (0) | 2026.01.30 |
| [02] 주요 자료 구조 (0) | 2026.01.27 |