본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
13장 : 제네릭·와일드카드·소거

생산자·소비자 와일드카드

extends 와일드카드에 값을 쓰고 super 와일드카드에서 구체 타입을 읽으려는 컴파일 실패를 재현하고 제네릭 메서드·비제한·상한·하한 와일드카드를 PECS로 선택합니다.

Box<Dog>Box<Animal>은 서로 대입되지 않습니다. 하지만 어떤 메서드는 Dog든 Cat이든 읽기만 하거나, 반대로 Animal 하위 값을 특정 상위 컨테이너에 쓰기만 합니다. 와일드카드는 제네릭 타입 정의를 바꾸지 않고 소비하는 메서드가 안전하게 허용할 타입 범위를 표현합니다.

생산자 와일드카드의 쓰기 제한

상한 와일드카드는 “정확한 타입은 모르지만 Animal의 어떤 하위 타입”입니다. 실제 객체가 Box<Cat>일 수도 있으므로 Dog를 쓰는 것은 안전하지 않습니다.

lab/ExtendsWildcardWriteFailure.java
public final class ExtendsWildcardWriteFailure {
    public static void main(String[] args) {
        Box<Dog> dogs = new Box<>();
        addDog(dogs);
    }

    private static void addDog(Box<? extends Animal> box) {
        box.set(new Dog("dog"));
    }

    private static final class Box<T> {
        private T value;
        void set(T value) { this.value = value; }
    }

    private interface Animal { String name(); }
    private record Dog(String name) implements Animal { }
}
javac 핵심 진단
error: incompatible types: Dog cannot be converted to CAP#1

CAP#1은 컴파일러가 캡처한 미지의 구체 타입입니다. null을 제외한 특정 Animal을 안전하게 넣을 근거가 없습니다. 대신 get 결과는 최소 Animal이라는 사실이 보장되므로 읽기 생산자에 적합합니다.

소비자 와일드카드의 읽기 제한

하한 와일드카드는 Dog 또는 그 부모 타입의 박스를 받습니다. 어느 경우든 Dog를 저장하는 것은 안전합니다. 그러나 실제 박스가 Box<Object>일 수 있으므로 꺼낸 값이 Dog라는 보장은 없고 정적 반환 타입은 Object입니다.

lab/SuperWildcardReadFailure.java
public final class SuperWildcardReadFailure {
    public static void main(String[] args) {
        Box<Animal> animals = new Box<>();
        Dog dog = readDog(animals);
        System.out.println(dog.name());
    }

    private static Dog readDog(Box<? super Dog> box) {
        box.set(new Dog("stored"));
        return box.get();
    }

    private static final class Box<T> {
        private T value;
        void set(T value) { this.value = value; }
        T get() { return value; }
    }

    private interface Animal { String name(); }
    private record Dog(String name) implements Animal { }
}
javac 핵심 진단
error: incompatible types: CAP#1 cannot be converted to Dog

하한 와일드카드는 소비자 역할에 적합합니다. Dog를 받아들이는 목적에는 충분하지만 구체 반환 관계까지 필요하면 타입 매개변수를 사용해야 합니다.

비제한 와일드카드

Box<?>Box<? extends Object>와 같은 허용 범위를 가지며 모든 Box를 받을 수 있습니다. 값의 toString, null 여부, 박스 자체의 공통 동작처럼 정확한 타입이 필요 없는 읽기에 적합합니다. 비매개변수화 타입과 달리 미지 타입이 존재한다는 사실을 보존하므로 임의 값을 넣지 못합니다.

src/WildcardReadAndCopy.java
import java.util.ArrayList;
import java.util.List;

public final class WildcardReadAndCopy {
    public static void main(String[] args) {
        List<Dog> source = List.of(new Dog("d1"), new Dog("d2"));
        List<Animal> target = new ArrayList<>();

        printAny(source);
        copyDogs(source, target);
        System.out.println("copied=" + target);
    }

    private static void printAny(List<?> values) {
        System.out.println("size=" + values.size() + ",first=" + values.getFirst());
    }

    private static void copyDogs(List<? extends Dog> source, List<? super Dog> target) {
        for (Dog dog : source) {
            target.add(dog);
        }
    }

    private interface Animal { String name(); }
    private record Dog(String name) implements Animal { }
}
size=2,first=Dog[name=d1]
copied=[Dog[name=d1], Dog[name=d2]]

source는 Dog 생산자라 extends, target은 Dog 소비자라 super입니다. PECS(Producer Extends, Consumer Super)는 이 방향을 기억하는 규칙입니다. 한 매개변수에서 읽고 쓰기를 모두 해야 하면 와일드카드보다 정확한 List<T>가 필요할 수 있습니다.

와일드카드와 제네릭 메서드

단지 값을 출력하고 부모 기능만 호출하면 와일드카드 일반 메서드가 더 단순합니다. 입력 박스의 정확한 타입을 반환까지 보존해야 하면 제네릭 메서드의 T가 필요합니다.

src/GenericMethodVersusWildcard.java
public final class GenericMethodVersusWildcard {
    public static void main(String[] args) {
        Box<Dog> box = new Box<>(new Dog("dori", "A-7"));

        Animal viewed = view(box);
        Dog preserved = preserve(box);

        System.out.println("view=" + viewed.name());
        System.out.println("code=" + preserved.code());
    }

    private static Animal view(Box<? extends Animal> box) {
        return box.get();
    }

    private static <T extends Animal> T preserve(Box<T> box) {
        return box.get();
    }

    private record Box<T>(T get) { }
    private interface Animal { String name(); }
    private record Dog(String name, String code) implements Animal { }
}
view=dori
code=A-7

T는 입력과 출력의 동일 타입 관계를 명명합니다. 와일드카드 메서드는 그런 관계를 새로 정의하지 않고 이미 타입 인자가 정해진 Box들을 넓게 받아 부모 관점으로 사용합니다.

타입 매개변수 이름 가림

제네릭 클래스 안에 <T> T method(T value)를 선언하면 메서드의 T가 클래스의 T와 별개이며 더 가까운 이름이 바깥 T를 가립니다. 서로 같은 타입처럼 보이지만 호출마다 전혀 다른 타입이 될 수 있습니다. 메서드 타입에는 R, U 같은 다른 이름을 사용하면 범위가 드러납니다.

예를 들어 class Converter<T> { <R> R convert(T source, Function<T,R> fn) }에서는 T가 인스턴스 입력 타입이고 R은 호출별 결과입니다. 같은 T 이름을 재사용하면 source와 result가 같은 타입이라는 잘못된 인상을 줍니다. 타입 변수 이름은 짧아도 관계 설명의 일부입니다.

와일드카드 선택표

  • 정확한 타입을 몰라도 읽기만 한다: List<?>
  • Animal 하위 값을 Animal로 생산한다: List<? extends Animal>
  • Dog 값을 상위 컨테이너에 소비시킨다: List<? super Dog>
  • 입력 타입을 반환에도 그대로 보존한다: <T> T method(...)
  • 객체 상태 전체가 같은 T를 공유한다: class Box<T>
  • 읽기와 쓰기를 모두 정확한 타입으로 한다: List<T>

와일드카드를 지나치게 중첩하면 호출자가 CAP 진단을 이해하기 어렵습니다. 공개 API는 흔한 생산자·소비자 변형만 제공하고 복잡한 캡처는 내부 도우미 제네릭 메서드로 옮길 수 있습니다. 반환 타입에 ? extends를 노출하면 호출자가 다시 미지 타입을 다뤄야 하므로 가능한 한 안정된 인터페이스나 명명된 T로 반환합니다.

연습 문제

copy(List<? extends T> source, List<? super T> target)를 작성하고 List<QuizResult>에서 List<Scored>로 복사하세요. target에서 읽을 때는 Scored 관점으로 점수를 합산합니다.

해설 보기
src/PecsCopyExercise.java
import java.util.ArrayList;
import java.util.List;

public final class PecsCopyExercise {
    public static void main(String[] args) {
        List<QuizResult> source = List.of(
                new QuizResult("Q1", 80), new QuizResult("Q2", 95));
        List<Scored> target = new ArrayList<>();

        copy(source, target);
        int total = target.stream().mapToInt(Scored::score).sum();
        System.out.println("count=" + target.size());
        System.out.println("total=" + total);
    }

    private static <T> void copy(List<? extends T> source, List<? super T> target) {
        for (T value : source) {
            target.add(value);
        }
    }

    private interface Scored { int score(); }
    private record QuizResult(String id, int score) implements Scored { }
}
count=2
total=175

T는 source 원소와 target에 추가할 값의 관계를 묶습니다. source는 생산하고 target은 소비하므로 PECS 방향과 일치합니다.