Notice
Recent Posts
Recent Comments
Link
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Tags more
Archives
Today
Total
관리 메뉴

프로그래밍 공부

[자바] #08. 와일드카드 (Wildcard) - 1. 제네릭 <T>와 와일드카드 (?) 본문

자바

[자바] #08. 와일드카드 (Wildcard) - 1. 제네릭 <T>와 와일드카드 (?)

하 냥 2025. 2. 15. 15:07

자바 제네릭에서 와일드카드는 타입 매개변수를 유연하게 설정할 때 사용된다.

📢 와일드카드 (?) 란?

?는 "아무 타입이나 올 수 있다"는 의미!

?를 사용하면 다양한 제네릭 타입을 허용할 수 있다.

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는 주로 제네릭 클래스나 메서드를 정의할 때 사용된다. 타입 매개변수로 사용자가 지정한 타입이 코드 전체에서 일관되게 적용된다.

?는 제네릭 클래스나 메서드를 사용할 때, 즉 제네릭 타입을 인스턴스화하거나 메서드를 호출할 때 사용되어 타입을 유연하게 관리할 수 있다.