본문 바로가기

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

와일드카드의 읽기·쓰기 실패를 재현하고 제네릭 메서드와 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>가 필요할 수 있습니다.

생산자라는 이름이 컬렉션의 불변성을 뜻하지는 않습니다. List<? extends Number>에서도 원소를 특정 Number로 새로 넣을 수는 없지만 remove·clear 같은 연산은 컴파일됩니다. 실제 수정 가능 여부는 전달한 구현의 계약입니다. 다음 프로그램은 별도 ArrayList와 List.of를 비교합니다.

예시

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

public final class WildcardMutationScope {
    public static void main(String[] args) {
        List<Integer> numbers = new ArrayList<>(List.of(1, 2));
        List<? extends Number> producer = numbers;
        Number first = producer.getFirst();
        producer.remove(0);
        System.out.println("first=" + first + ",remaining=" + numbers);
        producer.clear();
        System.out.println("after-clear=" + numbers.size());

        List<?> fixed = List.of(1);
        try {
            fixed.clear();
        } catch (UnsupportedOperationException error) {
            System.out.println("fixed=unmodifiable");
        }
    }
}
first=1,remaining=[2]
after-clear=0
fixed=unmodifiable

remove와 clear가 통과한 타입 검사는 List.of의 수정 지원을 보장하지 않습니다. List의 수정 불가 계약은 이 두 판단을 나누어 설명합니다. 원래 copyDogs의 List.of source는 읽기만 하고 수정 대상은 별도 ArrayList입니다.

와일드카드 안에서 허용되는 연산

extends 입력의 Dog 저장과 super 입력의 Dog 반환은 각각 컴파일 실패한다. List<?> 관찰 예제는 구체 타입을 몰라도 원소를 Object 관점에서 읽어 출력한다.

와일드카드 · 허용 연산

와일드카드 안에서 허용되는 연산

extends의 미지 타입에 Dog를 쓰거나 super의 읽기를 Dog로 반환할 수 없습니다. List<?>의 실제 관찰은 Object 관점입니다.

와일드카드 사용 계약과 실패 경계 독립된 세 원문입니다. extends Animal 박스에 Dog를 쓰는 시도는 알 수 없는 구체 타입 때문에 컴파일 실패합니다. super Dog 박스에서 Dog로 반환하려는 읽기는 Object 관찰 계약 때문에 컴파일 실패합니다. List<?>의 첫 원소 읽기는 Object 관찰로 정상 출력하며 나눈 size와 first 필드는 원 stdout 한 줄입니다. 와일드카드가 수정 불가 구현을 뜻하지는 않습니다. 원문 A · extends 쓰기 Box 읽기 관찰 Box<? extends Animal> Dog 넣기 시도 알 수 없는 구체 하위 T 컴파일 실패 Dog → CAP#1 불가 원문 B · super 읽기 Box에 Dog 쓰기 Box<? super Dog> get을 Dog로 반환 시도 읽기 계약은 Object 컴파일 실패 CAP#1 → Dog 불가 원문 C · List<?> 읽기 두 Dog의 목록 d1 다음 d2 printAny로 관찰 getFirst는 Object로 읽음 정상 출력 한 줄 size=2,first= Dog[name=d1] 영역마다 별도 원문 프로그램 실선: 사용과 검사·관찰 결과 와일드카드는 불변 목록 표시 아님
와일드카드 사용 계약과 실패 경계 독립된 세 원문입니다. extends Animal 박스에 Dog를 쓰는 시도는 알 수 없는 구체 타입 때문에 컴파일 실패합니다. super Dog 박스에서 Dog로 반환하려는 읽기는 Object 관찰 계약 때문에 컴파일 실패합니다. List<?>의 첫 원소 읽기는 Object 관찰로 정상 출력하며 나눈 size와 first 필드는 원 stdout 한 줄입니다. 와일드카드가 수정 불가 구현을 뜻하지는 않습니다. 원문 A · extends 쓰기 Box 읽기 관찰 Box<? extends Animal> Dog 넣기 시도 알 수 없는 구체 하위 T 컴파일 실패 Dog → CAP#1 불가 원문 B · super 읽기 Box에 Dog 쓰기 Box<? super Dog> get을 Dog로 반환 시도 읽기 계약은 Object 컴파일 실패 CAP#1 → Dog 불가 원문 C · List<?> 읽기 두 Dog의 목록 d1 다음 d2 printAny로 관찰 getFirst는 Object로 읽음 정상 출력 한 줄 size=2,first= Dog[name=d1] 영역마다 별도 원문 프로그램 실선: 사용과 검사·관찰 결과 와일드카드는 불변 목록 표시 아님
와일드카드 안에서 허용되는 연산
메서드의 매개변수알 수 있는 타입 관계예제의 연산과 결과
Box<? extends Animal>정확한 Animal 하위 타입은 모름
box.set(new Dog(...))
컴파일 실패
Dog를 그 미지 타입에 저장할 수 없음
Box<? super Dog>
Dog 저장은 허용
꺼낸 값의 보장 타입은 Object
return box.get()
Dog 반환에서 컴파일 실패
List<?>원소를 Object 관점에서 관찰
getFirst()로 첫 원소를 출력
size=2,first=Dog[name=d1]
Box<? extends Animal>
알 수 있는 타입 관계
정확한 Animal 하위 타입은 모름
예제의 연산과 결과
box.set(new Dog(...))
컴파일 실패
Dog를 그 미지 타입에 저장할 수 없음
Box<? super Dog>
알 수 있는 타입 관계
Dog 저장은 허용
꺼낸 값의 보장 타입은 Object
예제의 연산과 결과
return box.get()
Dog 반환에서 컴파일 실패
List<?>
알 수 있는 타입 관계
원소를 Object 관점에서 관찰
예제의 연산과 결과
getFirst()로 첫 원소를 출력
size=2,first=Dog[name=d1]

상한 와일드카드에도 null은 타입상 저장할 수 있습니다. 타입 검사가 허용하는 연산과 컬렉션 구현이 실제 수정을 지원하는지는 별개의 조건입니다.


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

단지 값을 출력하고 부모 기능만 호출하면 와일드카드 일반 메서드가 더 단순합니다.

입력 박스의 정확한 타입을 반환까지 보존해야 하면 제네릭 메서드의 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들을 넓게 받아 부모 관점으로 사용합니다.

같은 Box를 읽는 두 반환 계약

같은 Box<Dog>를 읽어도 view는 Animal을 반환하고 preserve는 입력의 T를 반환한다. main의 Dog preserved 참조는 code를 형변환 없이 호출한다.

반환 계약 · 같은 객체의 두 관점

같은 Box를 읽는 두 반환 계약

두 호출은 같은 dori를 읽습니다. view는 Animal 관점으로 반환하고 preserve는 입력의 T를 유지해 Dog의 code를 호출하게 합니다.

같은 Dog 박스의 부모 관찰과 T 보존 하나의 Box<Dog>에 dori A-7을 보관합니다. view는 같은 Dog를 Animal 선언으로 반환하여 view=dori를 읽고 preserve는 T=Dog로 반환하여 code=A-7을 읽습니다. 실선은 같은 박스를 두 호출에 전달하고 반환값을 관찰하는 관계입니다. 서로 다른 Dog 두 개를 만들지 않습니다. 한 Box<Dog> Dog dori / A-7 view의 부모 관찰 preserve의 T 관계 view 매개변수 Box<? extends Animal> preserve 매개변수 Box<T>, T는 Dog Animal로 반환 같은 Dog 객체 view=dori Dog로 반환 같은 Dog 객체 code=A-7 실선: 같은 박스를 두 호출에 전달 · 두 반환은 같은 Dog 객체 view의 부모 관찰과 preserve의 입력·반환 T 관계를 구별
같은 Dog 박스의 부모 관찰과 T 보존 하나의 Box<Dog>에 dori A-7을 보관합니다. view는 같은 Dog를 Animal 선언으로 반환하여 view=dori를 읽고 preserve는 T=Dog로 반환하여 code=A-7을 읽습니다. 실선은 같은 박스를 두 호출에 전달하고 반환값을 관찰하는 관계입니다. 서로 다른 Dog 두 개를 만들지 않습니다. 한 Box<Dog> Dog dori / A-7 view의 부모 관찰 view 매개변수 Box<? extends Animal> Animal로 반환 같은 Dog 객체 view=dori preserve의 T 관계 preserve 매개변수 Box<T>, T는 Dog Dog로 반환 같은 Dog 객체 code=A-7 실선: 같은 박스를 각 호출에 전달 두 반환은 같은 Dog를 가리킴 다른 객체 두 개를 만들지 않음

view(box)
Box<? extends Animal> 입력

main에서 받는 타입
Animal viewed
원문에서 사용하는 기능
viewed.name()
view=dori

preserve(box)
Box<T> 입력, T 반환

main에서 받는 타입
Dog preserved
원문에서 사용하는 기능
preserved.code()
code=A-7

두 호출은 같은 Dog dori를 읽습니다. 달라지는 것은 결과를 통해 컴파일러가 허용하는 멤버의 범위입니다.


타입 매개변수 이름 가림

제네릭 클래스 안에 <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<ScoredPost>에서 List<Scored>로 복사하세요.

이 연습에서는 copy의 선언으로 source에서 읽을 타입과 target에 쓸 타입을 연결합니다. 점수 합산은 3장에서 배운 향상된 for문으로 확인하세요. 복사 후 target의 원소 수와 점수 합계를 먼저 예상해 보세요.

연습 입력은 ScoredPost(101L, 80), ScoredPost(102L, 95) 두 개이며 Scored는 score()를 제공합니다. target은 처음 비어 있는 수정 가능한 ArrayList<Scored>입니다. copy는 source 순서대로 원소를 target 끝에 추가하고 source를 바꾸지 않습니다. 중복 제거·초기화·새 ScoredPost 생성은 요구하지 않습니다.

독립 조건에서는 target에 점수 10인 기존 ScoredPost 하나가 있고 같은 두 source를 한 번 추가합니다. 그 뒤 count와 total은 각각 얼마인가요? 두 번째 copy를 다시 실행하면 첫 결과를 덮어쓸지도 함께 예상하세요.

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

public final class PecsCopyExercise {
    public static void main(String[] args) {
        List<ScoredPost> source = List.of(
                new ScoredPost(101L, 80), new ScoredPost(102L, 95));
        List<Scored> target = new ArrayList<>();

        copy(source, target);
        int total = 0;
        for (Scored value : target) {
            total += value.score();
        }
        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 ScoredPost(long postId, int score) implements Scored { }
}
count=2
total=175

total은 0에서 시작해 첫 원소의 80점, 두 번째 원소의 95점을 더해 175가 됩니다. count가 2가 아니라면 copy가 source의 두 원소를 모두 추가했는지 확인하세요.

T는 source 원소와 target에 추가할 값의 관계를 묶습니다.

source는 생산하고 target은 소비하므로 PECS 방향과 일치합니다.

독립 조건에서는 기존 1개에 2개를 더해 count=3, total=10+80+95=185입니다. 같은 copy를 다시 하면 2개가 더 붙어 count=5, total=360입니다. 이 구현은 같은 입력을 다시 넣어도 한 번만 남기는 처리가 아닙니다.

copy는 객체 참조를 추가합니다. 새 리스트에 보관해도 각 ScoredPost를 새로 복제하지 않습니다. 원래 record의 long·int 값은 불변이지만, 다른 가변 원소 타입을 사용할 때에는 원소 상태를 공유할 수 있다는 조건을 따로 확인해야 합니다.

copy 안의 target은 List<? super T>이므로 읽은 값에 대해 보장되는 타입은 Object입니다. main에서 선언한 target의 타입은 여전히 List<Scored>이므로 위 반복문에서는 Scored로 읽어 score()를 호출할 수 있습니다.

복사 메서드와 호출자가 보는 타입

source의 두 참조를 target에 copy한 뒤 Scored로 읽어 175를 누산합니다. PECS의 공급/소비 역할을 유지하며 목록과 합계의 실제 전후를 보여 줄 수 있습니다.

복사 메서드와 호출자가 보는 타입

copy의 source는 T를 생산하고 target은 T를 소비한다. 호출자의 target 변수는 List<Scored>여서 복사 후 Scored 점수를 합산할 수 있다.

PECS · 메서드와 호출자 관점

복사 메서드와 호출자가 보는 타입

copy 안에서 source는 T를 생산하고 target은 T를 받습니다. main의 target 선언은 여전히 List<Scored>여서 점수를 읽을 수 있습니다.

메서드의 읽기·쓰기 계약과 호출자 목록source List<ScoredPost>의 두 참조를 extends T로 순회하고 super T target 끝에 추가합니다. 호출자의 target 선언은 List<Scored> 그대로이며 score 합계 175와 count 2를 읽습니다. 실선은 순회·추가·호출자 읽기의 연결입니다. 원소를 새 객체로 복제하거나 호출자 target을 List<Object>로 바꾸지 않습니다.
호출자의 sourceList<ScoredPost>101 / 80, 102 / 95
연결 관계호출자의 source · List<ScoredPost> · 101 / 80, 102 / 95 → copy의 읽기 계약 · List<? extends T> · source의 두 참조를 순회
copy의 읽기 계약List<? extends T>source의 두 참조를 순회
연결 관계copy의 읽기 계약 · List<? extends T> · source의 두 참조를 순회 → copy의 쓰기 계약 · List<? super T> · target.add(value)
copy의 쓰기 계약List<? super T>target.add(value)
연결 관계copy의 쓰기 계약 · List<? super T> · target.add(value) → 호출자의 target · List<Scored> · 같은 두 객체 참조 보관
호출자의 targetList<Scored>같은 두 객체 참조 보관
연결 관계호출자의 target · List<Scored> · 같은 두 객체 참조 보관 → Scored로 읽기 · score로 80 + 95 · count=2 / total=175
Scored로 읽기score로 80 + 95count=2 / total=175

실선: source 순회 → target에 참조 추가 → 호출자가 Scored로 읽기와일드카드는 메서드의 사용 계약 · caller target 타입을 Object로 바꾸거나 원소를 복제하지 않음

메서드의 읽기·쓰기 계약과 호출자 목록source List<ScoredPost>의 두 참조를 extends T로 순회하고 super T target 끝에 추가합니다. 호출자의 target 선언은 List<Scored> 그대로이며 score 합계 175와 count 2를 읽습니다. 실선은 순회·추가·호출자 읽기의 연결입니다. 원소를 새 객체로 복제하거나 호출자 target을 List<Object>로 바꾸지 않습니다.
호출자의 sourceList<ScoredPost>101 / 80, 102 / 95
연결 관계호출자의 source · List<ScoredPost> · 101 / 80, 102 / 95 → copy의 읽기 계약 · List<? extends T> · source의 두 참조를 순회
copy의 읽기 계약List<? extends T>source의 두 참조를 순회
연결 관계copy의 읽기 계약 · List<? extends T> · source의 두 참조를 순회 → copy의 쓰기 계약 · List<? super T> · target.add(value)
copy의 쓰기 계약List<? super T>target.add(value)
연결 관계copy의 쓰기 계약 · List<? super T> · target.add(value) → 호출자의 target · List<Scored> · 같은 두 객체 참조 보관
호출자의 targetList<Scored>같은 두 객체 참조 보관
연결 관계호출자의 target · List<Scored> · 같은 두 객체 참조 보관 → Scored로 읽기 · score로 80 + 95 · count=2 / total=175
Scored로 읽기score로 80 + 95count=2 / total=175

실선: 순회·추가·읽기 연결원소 객체를 새로 복제하지 않음target의 선언 타입은 그대로

copy 안의 source

보이는 타입
List<? extends T>
가능한 동작
for (T value : source)
원소를 T로 읽음

copy 안의 target

보이는 타입
List<? super T>
가능한 동작
target.add(value)
T 값을 추가

main의 target

보이는 타입
List<Scored>
가능한 동작
for (Scored value : target)
value.score()로 합산
count=2
total=175

copy의 하한 와일드카드로 꺼낸 값은 Object 관점입니다. 호출자에게는 원래 List<Scored> 선언이 남아 있으므로 Scored로 읽을 수 있습니다.

이 예의 조건

  • 객체 원소를 복제하거나 caller target 타입을 바꾸지 않는다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
5101
680, 102
795
원 도표의 설명과 주석
구분내용
전체 읽기 정보복사 메서드와 호출자가 보는 타입 복사 메서드와 호출자가 보는 타입 copy의 source는 T를 생산하고 target은 T를 소비한다. 호출자의 target 변수는 List<Scored>여서 복사 후 Scored 점수를 합산할 수 있다. PECS · 메서드와 호출자 관점 복사 메서드와 호출자가 보는 타입 copy 안에서 source는 T를 생산하고 target은 T를 받습니다. main의 target 선언은 여전히 List<Scored>여서 점수를 읽을 수 있습니다. copy 안의 source 보이는 타입 List<? extends T> 가능한 동작 for (T value : source) 원소를 T로 읽음 copy 안의 target 보이는 타입 List<? super T> 가능한 동작 target.add(value) T 값을 추가 main 의 target 보이는 타입 List<Scored> 가능한 동작 for (Scored value : target) value.score() 로 합산 count=2 total=175 copy 의 하한 와일드카드로 꺼낸 값은 Object 관점입니다. 호출자에게는 원래 List<Scored> 선언이 남아 있으므로 Scored로 읽을 수 있습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
읽기 준비

ScoredPost 두 개를 extends T로 읽습니다.

  • source: 101/80,102/95
  • target 선언: List<Scored>
  • 메서드의 와일드카드가 읽기/쓰기 방향을 제한한다.
참조 추가

source의 두 참조를 super T 대상 끝에 추가합니다.

  • target count: 2
  • 객체 정체성: source와 같은 두 객체
  • 대상에 객체 복제 대신 기존 참조를 보관한다.
호출자 누산

Scored 계약으로 80과 95를 읽습니다.

  • total: 175
  • count: 2
  • caller의 선언 타입으로 허용된 행동을 사용한다.

복사 메서드와 호출자가 보는 타입

source의 두 참조를 target에 copy한 뒤 Scored로 읽어 175를 누산합니다. PECS의 공급/소비 역할을 유지하며 목록과 합계의 실제 전후를 보여 줄 수 있습니다.

복사 메서드와 호출자가 보는 타입

copy의 source는 T를 생산하고 target은 T를 소비한다. 호출자의 target 변수는 List<Scored>여서 복사 후 Scored 점수를 합산할 수 있다.

PECS · 메서드와 호출자 관점

복사 메서드와 호출자가 보는 타입

copy 안에서 source는 T를 생산하고 target은 T를 받습니다. main의 target 선언은 여전히 List<Scored>여서 점수를 읽을 수 있습니다.

메서드의 읽기·쓰기 계약과 호출자 목록source List<ScoredPost>의 두 참조를 extends T로 순회하고 super T target 끝에 추가합니다. 호출자의 target 선언은 List<Scored> 그대로이며 score 합계 175와 count 2를 읽습니다. 실선은 순회·추가·호출자 읽기의 연결입니다. 원소를 새 객체로 복제하거나 호출자 target을 List<Object>로 바꾸지 않습니다.
호출자의 sourceList<ScoredPost>101 / 80, 102 / 95
연결 관계호출자의 source · List<ScoredPost> · 101 / 80, 102 / 95 → copy의 읽기 계약 · List<? extends T> · source의 두 참조를 순회
copy의 읽기 계약List<? extends T>source의 두 참조를 순회
연결 관계copy의 읽기 계약 · List<? extends T> · source의 두 참조를 순회 → copy의 쓰기 계약 · List<? super T> · target.add(value)
copy의 쓰기 계약List<? super T>target.add(value)
연결 관계copy의 쓰기 계약 · List<? super T> · target.add(value) → 호출자의 target · List<Scored> · 같은 두 객체 참조 보관
호출자의 targetList<Scored>같은 두 객체 참조 보관
연결 관계호출자의 target · List<Scored> · 같은 두 객체 참조 보관 → Scored로 읽기 · score로 80 + 95 · count=2 / total=175
Scored로 읽기score로 80 + 95count=2 / total=175

실선: source 순회 → target에 참조 추가 → 호출자가 Scored로 읽기와일드카드는 메서드의 사용 계약 · caller target 타입을 Object로 바꾸거나 원소를 복제하지 않음

메서드의 읽기·쓰기 계약과 호출자 목록source List<ScoredPost>의 두 참조를 extends T로 순회하고 super T target 끝에 추가합니다. 호출자의 target 선언은 List<Scored> 그대로이며 score 합계 175와 count 2를 읽습니다. 실선은 순회·추가·호출자 읽기의 연결입니다. 원소를 새 객체로 복제하거나 호출자 target을 List<Object>로 바꾸지 않습니다.
호출자의 sourceList<ScoredPost>101 / 80, 102 / 95
연결 관계호출자의 source · List<ScoredPost> · 101 / 80, 102 / 95 → copy의 읽기 계약 · List<? extends T> · source의 두 참조를 순회
copy의 읽기 계약List<? extends T>source의 두 참조를 순회
연결 관계copy의 읽기 계약 · List<? extends T> · source의 두 참조를 순회 → copy의 쓰기 계약 · List<? super T> · target.add(value)
copy의 쓰기 계약List<? super T>target.add(value)
연결 관계copy의 쓰기 계약 · List<? super T> · target.add(value) → 호출자의 target · List<Scored> · 같은 두 객체 참조 보관
호출자의 targetList<Scored>같은 두 객체 참조 보관
연결 관계호출자의 target · List<Scored> · 같은 두 객체 참조 보관 → Scored로 읽기 · score로 80 + 95 · count=2 / total=175
Scored로 읽기score로 80 + 95count=2 / total=175

실선: 순회·추가·읽기 연결원소 객체를 새로 복제하지 않음target의 선언 타입은 그대로

copy 안의 source

보이는 타입
List<? extends T>
가능한 동작
for (T value : source)
원소를 T로 읽음

copy 안의 target

보이는 타입
List<? super T>
가능한 동작
target.add(value)
T 값을 추가

main의 target

보이는 타입
List<Scored>
가능한 동작
for (Scored value : target)
value.score()로 합산
count=2
total=175

copy의 하한 와일드카드로 꺼낸 값은 Object 관점입니다. 호출자에게는 원래 List<Scored> 선언이 남아 있으므로 Scored로 읽을 수 있습니다.

이 예의 조건

  • 객체 원소를 복제하거나 caller target 타입을 바꾸지 않는다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
5101
680, 102
795
원 도표의 설명과 주석
구분내용
전체 읽기 정보복사 메서드와 호출자가 보는 타입 복사 메서드와 호출자가 보는 타입 copy의 source는 T를 생산하고 target은 T를 소비한다. 호출자의 target 변수는 List<Scored>여서 복사 후 Scored 점수를 합산할 수 있다. PECS · 메서드와 호출자 관점 복사 메서드와 호출자가 보는 타입 copy 안에서 source는 T를 생산하고 target은 T를 받습니다. main의 target 선언은 여전히 List<Scored>여서 점수를 읽을 수 있습니다. copy 안의 source 보이는 타입 List<? extends T> 가능한 동작 for (T value : source) 원소를 T로 읽음 copy 안의 target 보이는 타입 List<? super T> 가능한 동작 target.add(value) T 값을 추가 main 의 target 보이는 타입 List<Scored> 가능한 동작 for (Scored value : target) value.score() 로 합산 count=2 total=175 copy 의 하한 와일드카드로 꺼낸 값은 Object 관점입니다. 호출자에게는 원래 List<Scored> 선언이 남아 있으므로 Scored로 읽을 수 있습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
읽기 준비

ScoredPost 두 개를 extends T로 읽습니다.

  • source: 101/80,102/95
  • target 선언: List<Scored>
  • 메서드의 와일드카드가 읽기/쓰기 방향을 제한한다.
참조 추가

source의 두 참조를 super T 대상 끝에 추가합니다.

  • target count: 2
  • 객체 정체성: source와 같은 두 객체
  • 대상에 객체 복제 대신 기존 참조를 보관한다.
호출자 누산

Scored 계약으로 80과 95를 읽습니다.

  • total: 175
  • count: 2
  • caller의 선언 타입으로 허용된 행동을 사용한다.