13. 제네릭
제네릭(generic) 은 "어떤 타입을 다룰지" 를 **클래스/메서드 정의 시점에 미루는** 메커니즘입니다. `List<String>` 의 꺾쇠가 바로 그 자리 표시자입니다. 컴파일 시점에 타입을 강제해 안전하고, 다양한 타입을 다룰 수 있어 재사용성도 높습니다.
이 강의에서 배우는 것
- 1제네릭 클래스 `Box<T>` 를 정의한다
- 2제네릭 메서드 `<T> T choose(T a, T b)` 를 작성한다
- 3와일드카드 `?`, `? extends T`, `? super T` 를 구분한다
- 4bounded type (`<T extends Number>`) 의 효과를 안다
- 5타입 소거(type erasure) 의 개념을 살짝 본다
소개
제네릭(generic) 은 "어떤 타입을 다룰지" 를 **클래스/메서드 정의 시점에 미루는** 메커니즘입니다. `List<String>` 의 꺾쇠가 바로 그 자리 표시자입니다. 컴파일 시점에 타입을 강제해 안전하고, 다양한 타입을 다룰 수 있어 재사용성도 높습니다.
핵심 개념
1) 제네릭 클래스
public class Box<T> {
private T value;
public void set(T v) { this.value = v; }
public T get() { return value; }
}
Box<String> b = new Box<>();
b.set("hi");
String s = b.get(); // 캐스팅 불필요2) 제네릭 메서드
static <T> T choose(boolean first, T a, T b) {
return first ? a : b;
}
String r = choose(true, "yes", "no");3) 와일드카드
List<? extends Number> nums = ...; // Number 또는 그 자식 (읽기 전용에 가까움)
List<? super Integer> ints = ...; // Integer 또는 그 부모 (쓰기 가능)**PECS**: Producer-Extends, Consumer-Super 라는 암기 규칙이 있습니다. 값을 **꺼내쓸 때 `extends`**, **넣어줄 때 `super`**.
4) bounded type
static <T extends Number> double sum(List<T> xs) {
double s = 0;
for (T x : xs) s += x.doubleValue();
return s;
}`Number` 또는 그 자식만 받을 수 있어 `doubleValue()` 같은 부모 메서드를 안전하게 호출할 수 있습니다.
5) 타입 소거
런타임에는 제네릭 정보가 사라집니다 (`List<String>` ↔ `List<Integer>` 가 같은 클래스로 보임). 따라서 `new T()` 같은 코드는 불가능합니다.
핵심 예제
예제 1 — `BoxGeneric.java` : 제네릭 클래스
public class BoxGeneric {
static class Box<T> {
private T value;
public void set(T v) { this.value = v; }
public T get() { return value; }
}
public static void main(String[] args) {
Box<String> s = new Box<>();
s.set("Hello");
System.out.println(s.get());
Box<Integer> n = new Box<>();
n.set(42);
System.out.println(n.get() + 1);
}
}**실행 결과**
Hello
43**메모:** 다이아몬드 연산자 `<>` 가 우항 타입을 추론합니다.
예제 2 — `GenericMethod.java` : 제네릭 메서드
import java.util.List;
public class GenericMethod {
static <T> T first(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
public static void main(String[] args) {
System.out.println(first(List.of(1, 2, 3)));
System.out.println(first(List.of("a", "b")));
Object none = first(List.of());
System.out.println(none);
}
}**실행 결과**
1
a
null**메모:** 메서드 시그니처 안의 `<T>` 는 메서드 호출 시 추론됩니다.
예제 3 — `Wildcards.java` : `? extends` 와 `? super`
import java.util.ArrayList;
import java.util.List;
public class Wildcards {
/** 어떤 숫자 리스트든 합산. */
static double sum(List<? extends Number> xs) {
double s = 0;
for (Number n : xs) s += n.doubleValue();
return s;
}
/** Integer 또는 그 부모 자리에 1~3 추가. */
static void addNums(List<? super Integer> xs) {
xs.add(1); xs.add(2); xs.add(3);
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3)));
System.out.println(sum(List.of(1.5, 2.5)));
List<Number> ns = new ArrayList<>();
addNums(ns);
System.out.println(ns);
}
}**실행 결과**
6.0
4.0
[1, 2, 3]**메모:** **꺼내 쓸 때 `extends`**, **넣어 줄 때 `super`**.
예제 4 — `BoundedType.java` : `T extends Number`
import java.util.List;
public class BoundedType {
static <T extends Number> double average(List<T> xs) {
if (xs.isEmpty()) return 0;
double s = 0;
for (T x : xs) s += x.doubleValue();
return s / xs.size();
}
public static void main(String[] args) {
System.out.println(average(List.of(10, 20, 30)));
System.out.println(average(List.of(1.5, 2.5, 3.5)));
}
}**실행 결과**
20.0
2.5**메모:** `T extends Number` 덕분에 `Integer`/`Double`/`Long` 등을 한 메서드로 처리할 수 있습니다.
전체 예제 코드 (src/)
src/BoundedType.java
import java.util.List;
public class BoundedType {
static <T extends Number> double average(List<T> xs) {
if (xs.isEmpty()) return 0;
double s = 0;
for (T x : xs) s += x.doubleValue();
return s / xs.size();
}
public static void main(String[] args) {
System.out.println(average(List.of(10, 20, 30)));
System.out.println(average(List.of(1.5, 2.5, 3.5)));
}
}
src/BoxGeneric.java
public class BoxGeneric {
static class Box<T> {
private T value;
public void set(T v) { this.value = v; }
public T get() { return value; }
}
public static void main(String[] args) {
Box<String> s = new Box<>();
s.set("Hello");
System.out.println(s.get());
Box<Integer> n = new Box<>();
n.set(42);
System.out.println(n.get() + 1);
}
}
src/GenericMethod.java
import java.util.List;
public class GenericMethod {
static <T> T first(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
public static void main(String[] args) {
System.out.println(first(List.of(1, 2, 3)));
System.out.println(first(List.of("a", "b")));
Object none = first(List.of());
System.out.println(none);
}
}
src/Wildcards.java
import java.util.ArrayList;
import java.util.List;
public class Wildcards {
/** 어떤 숫자 리스트든 합산. */
static double sum(List<? extends Number> xs) {
double s = 0;
for (Number n : xs) s += n.doubleValue();
return s;
}
/** Integer 또는 그 부모 자리에 1~3 추가. */
static void addNums(List<? super Integer> xs) {
xs.add(1); xs.add(2); xs.add(3);
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3)));
System.out.println(sum(List.of(1.5, 2.5)));
List<Number> ns = new ArrayList<>();
addNums(ns);
System.out.println(ns);
}
}
자주 하는 실수
- `Box<Object>` 와 `Box<String>` 이 호환된다고 착각 (서로 무관)
- `new T()` / `new T[10]` 시도 → 타입 소거로 인해 불가
- raw type 사용 (`List list = ...`) → 컴파일 경고 + 런타임 위험
- `? extends T` 컬렉션에 `add` 호출 (대부분 막힘)
- 와일드카드를 메서드 반환 타입에 남발 → 호출 측 불편
정리
- 제네릭은 컴파일 타임 타입 안전성을 위한 도구
- `<T>` 는 클래스/메서드 레벨에서 사용 가능
- 와일드카드는 PECS 규칙으로 외우면 편함
- 런타임에는 타입 소거되므로 일부 제약 존재
과제
# 과제 - 13. 제네릭
## 문제 1 — `Pair<A, B>`
- 파일명: `Homework01.java`
- 핵심 개념: 제네릭 클래스, 두 개의 타입 매개변수
요구사항
- 제네릭 클래스 `Pair<A, B>` 정의 (생성자 + getter 2개 + `toString`).
- `(1, "one")`, `("k", 3.14)` 두 가지를 만들고 출력.
예상 출력
(1, one)
(k, 3.14)## 문제 2 — 제네릭 메서드 `max`
- 파일명: `Homework02.java`
- 핵심 개념: 제네릭 메서드, `Comparable`
요구사항
- `<T extends Comparable<T>> T max(List<T> xs)` 정의 (빈 리스트면 null).
- `Integer`, `String` 리스트 둘 다 호출해 출력.
예상 출력
9
ZZZ
null## 정답 확인 직접 풀어 본 후 [`answer/`](./answer/) 폴더의 정답과 비교해 보세요.
정답 코드 (homework/answer/)
answer/Homework01.java
/** Pair<A,B> 제네릭 클래스. */
public class Homework01 {
static class Pair<A, B> {
private final A first;
private final B second;
Pair(A first, B second) {
this.first = first;
this.second = second;
}
A first() { return first; }
B second() { return second; }
@Override
public String toString() {
return "(" + first + ", " + second + ")";
}
}
public static void main(String[] args) {
Pair<Integer, String> p1 = new Pair<>(1, "one");
Pair<String, Double> p2 = new Pair<>("k", 3.14);
System.out.println(p1);
System.out.println(p2);
}
}
answer/Homework02.java
import java.util.List;
/** 제네릭 메서드 max. */
public class Homework02 {
static <T extends Comparable<T>> T max(List<T> xs) {
if (xs.isEmpty()) return null;
T best = xs.get(0);
for (T x : xs) if (x.compareTo(best) > 0) best = x;
return best;
}
public static void main(String[] args) {
System.out.println(max(List.of(3, 9, 1, 7)));
System.out.println(max(List.of("ABC", "ZZZ", "Yellow")));
System.out.println(max(List.<Integer>of()));
}
}
직접 해 보기
cd 03_컬렉션_제네릭/13_제네릭/src
javac BoxGeneric.java
java BoxGeneric다음 단원
[14_Stream_API](../14_Stream_API/) — 컬렉션을 우아하게 다루는 Stream 을 배웁니다.