프로그래밍 공부
[자바] #08. 와일드카드 (Wildcard) - 1. 제네릭 <T>와 와일드카드 (?) 본문
자바 제네릭에서 와일드카드는 타입 매개변수를 유연하게 설정할 때 사용된다.
📢 와일드카드 (?) 란?
?는 "아무 타입이나 올 수 있다"는 의미!
?를 사용하면 다양한 제네릭 타입을 허용할 수 있다.
class Box<T> { ... } // 일반적인 제네릭 클래스
class Box<?> { ... } // 와일드카드(?) 사용: 모든 타입 허용
| 와일드카드 | 의미 | 예제 |
| <?> | 모든 타입 허용 | Box<?> |
| <? extends T> | T와 T의 하위 타입만 허용 | Box<? extends Number> |
| <? super T> | T와 T의 상위 타입만 허용 | Box<? super Integer> |
👀 <?> - 모든 타입 허용
<?>는 "모든 타입을 받을 수 있다"는 의미.
Box<?>는 Box<String>, Box<Integer>, Box<Double> 등 모든 타입을 받을 수 있다
class Box<T> {
private T value;
public void setValue(T value) { this.value = value; }
public T getValue() { return value; }
}
public class Main {
public static void main(String[] args) {
Box<String> strBox = new Box<>();
strBox.setValue("Hello");
Box<?> unknownBox = strBox; // 모든 타입을 받을 수 있음
System.out.println(unknownBox.getValue()); // 값 읽기 가능
}
}
✅ Box<?>를 사용하면 Box<String>, Box<Integer> 등 어떤 타입이든 받을 수 있다.
✅ 단, ? 타입의 값을 가져올 수는 있지만, 저장할 수는 없다! (box.setValue(...) 불가능)
-> 이게 무슨 말일까?
❓ 왜 setValue()를 호출할 수 없는가?
class Box<T> {
private T value;
public void setValue(T value) { this.value = value; }
public T getValue() { return value; }
}
public class Main {
public static void main(String[] args) {
Box<?> unknownBox = new Box<String>();
unknownBox.setValue("Hello"); // ❌ 컴파일 오류 발생
unknownBox.setValue(100); // ❌ 컴파일 오류 발생
}
}
✅ Box<?>가 Box<String>인지, Box<Integer>인지 모르기 때문에, setValue("Hello")도, setValue(100)도 불가능
✅ 만약 Box<?>가 Box<Integer>라면 setValue("Hello")는 잘못된 타입이므로 문제가 된다.
✅ 즉, 타입 안정성을 유지하기 위해, setValue() 호출을 금지한다.
📌 ?는 Object 타입으로 다루지만, setValue(Object o)는 아니다
Box<?> unknownBox = new Box<String>();
Object obj = unknownBox.getValue(); // ✅ Object 타입으로 값 가져오기 가능
unknownBox.setValue(new Object()); // ❌ 불가능!
getValue()를 호출하면 어떤 타입이든 Object로 업캐스팅되므로 안전하게 가져올 수 있다.
하지만 setValue(Object o)는 허용되지 않음. Box<String>일 수도 있고, Box<Integer>일 수도 있기 때문이다.
또 질문 -> ❓ getValue()의 반환형이 T인데 왜 Object로 업캐스팅되어 반환되는가?
이 질문의 핵심은 와일드카드(?)가 어떻게 타입을 처리하는지와 컴파일러가 이를 어떻게 해석하는지에 있다.
Box<String> strBox = new Box<>();
strBox.setValue("Hello");
Box<?> unknownBox = strBox; // 와일드카드 사용
Object obj = unknownBox.getValue(); // ✅ 반환 타입이 Object로 변함
unknownBox는 Box<?> 타입이므로, <?>는 어떤 타입이든 올 수 있다는 의미.
getValue()의 원래 반환 타입은 T지만, Box<?>에서는 T가 어떤 타입인지 알 수 없다.
따라서, T의 구체적인 타입을 몰라서 Object로 업캐스팅된다.
해결 방법은 (String) unknownBox.getValue();로 다운캐스팅이 필요하다.
❓ 제네릭 T vs 와일드카드 (?) 차이점
일단, T는 클래스나 메서드 선언부에서 주로 사용하고 와일드카드는 메서드의 매개변수로 주로 사용한다.
※ 언제 T를 쓰고 언제 ?를 쓸까?
✅ T를 사용할 때 (제네릭 타입)
→
- 메서드 내부에서 타입이 고정되어야 할 때
- 읽기와 쓰기가 모두 필요할 때
- 같은 타입을 유지해야 할 때
class Box<T> {
private T item;
public void set(T item) { // T 타입이 고정됨 (쓰기 가능)
this.item = item;
}
public T get() { // 같은 T 타입 반환 (읽기 가능)
return item;
}
}
- T는 한 번 정해지면 변하지 않고, 읽기/쓰기 모두 가능하다.
- Box<Integer>, Box<String>처럼 특정 타입으로 객체를 만들 수 있다.
📌 적용 예시
- List<T>, Map<K, V>, Optional<T>처럼 타입을 명확히 지정해야 하는 경우
- 메서드 내에서 같은 타입을 유지해야 하는 경우
✅ 와일드카드(?) 를 사용할 때
→
- 메서드가 여러 타입을 받아야 할 때
- 읽기 전용(? extends)이거나, 특정 타입 이상만 허용(? super)할 때
- 타입이 중요하지 않고, 특정 연산(출력, 변환 등)만 수행할 때
public void printList(List<?> list) { // 어떤 타입의 List든 받을 수 있음
for (Object obj : list) {
System.out.println(obj);
}
}
- List<?>는 어떤 타입이든 받을 수 있지만, 쓰기(set)는 불가능하다.
(왜냐하면 어떤 타입이 올지 모르기 때문) - list.add(값) 같은 연산을 하면 타입 안정성을 보장할 수 없기 때문에 읽기 전용으로 사용된다.
하지만 와일드카드(?)는 무조건 읽기 전용이다! 라고 하기엔, 또 그건 아니다.
사실 <? extends T>와 <? super T>에 따라 읽기/쓰기 가능 여부가 달라진다.
이는 다음 편에서 작성하겠다.
*️⃣ T와 와일드카드 사용 예제 비교
🎯 1. 데이터를 변환하는 메서드 (T 사용)
public static <T> List<T> copy(List<T> source) {
List<T> result = new ArrayList<>();
for (T item : source) {
result.add(item);
}
return result;
}
왜 T를 써야 할까?
- List<T>의 타입이 String이면 결과도 List<String>
- List<Integer>면 결과도 List<Integer>
- 즉, 입력(매개변수)과 출력(반환값)이 같은 타입이어야 할 때 T를 사용한다!
❓ 입력과 출력이 같은 타입?
public static List<?> copy(List<?> source) {
List<?> result = new ArrayList<>(source);
return result; // ❌ 반환 타입이 `List<?>`이라서 사용하기 어려움
}
List<String>을 넣어도 반환값이 List<?>이므로 타입을 알 수 없다.
List<String> strings = List.of("A", "B", "C");
List<?> copiedList = copy(strings);
String s = copiedList.get(0); // ❌ ERROR! (Object 타입으로 반환됨)
T를 쓰면, 입력 타입이 String이면 반환 타입도 String이므로 문제 없음!
하지만 ?를 쓰면 반환 타입이 ?라서 원하는 타입으로 활용하기 어렵다.
🎯 2. 데이터를 출력하는 메서드 (? 사용)
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
왜 ?를 써야 할까?
- List<Integer>, List<String>, List<Double> 어떤 리스트든 받을 수 있음!
- 읽기만 필요하고, 쓰기는 안 함
- 특정 타입을 강제할 필요가 없음
요약하자면:
✅ T는 제네릭 클래스나 메서드를 정의할 때 타입을 고정하고, 그 타입을 여러 곳에서 일관되게 사용할 수 있다. 따라서 타입 안전성이 높다.
✅ ?는 타입을 고정하지 않고, 그 범위 내에서 불특정 타입을 받아들일 수 있어 더 유연하지만, 내부에서 특정 타입으로 사용하는 데 제한이 있다.
✅ T는 주로 제네릭 클래스나 메서드를 정의할 때 사용된다. 타입 매개변수로 사용자가 지정한 타입이 코드 전체에서 일관되게 적용된다.
✅ ?는 제네릭 클래스나 메서드를 사용할 때, 즉 제네릭 타입을 인스턴스화하거나 메서드를 호출할 때 사용되어 타입을 유연하게 관리할 수 있다.
'자바' 카테고리의 다른 글
| [자바] #10. 컬렉션 프레임워크 - 1. 컬렉션에 대한 전반적인 이해와 ArrayList에 대해 (0) | 2025.02.16 |
|---|---|
| [자바] #09. 와일드카드 (Wildcard) - 2. 상한 제한과 하한 제한 (0) | 2025.02.15 |
| [자바] #07. 제네릭 (Generics) (0) | 2025.02.15 |
| [자바] #06. 배열의 정렬 sort (0) | 2025.02.15 |
| [자바] #05. Object 클래스에 정의되어 있는 주요 메소드 (equals, clone) (0) | 2025.02.14 |