An extends wildcard permits reading values as a bound; a super wildcard permits writing values of that bound. A copy method can combine both without claiming two lists have the same element type.
Java generic wildcards: copy from a producer into a consumer
Operational contract
The source produces ParcelEvent values or a subtype. The destination consumes ParcelEvent values and may be a list of ParcelEvent or Object. The method rejects a source over 47 entries before changing the destination, then appends each element. It does not promise all-or-nothing delivery: a destination that throws during add may already contain a prefix. When atomic publication matters, build a separate immutable result and replace one owner reference instead. The wildcard bounds express allowed operations at compile time; they do not check runtime business rules on each event.
Failure case
A depot has a List of ScannedParcel values and needs to add them to a List of ParcelEvent values. List<ScannedParcel> cannot be assigned to List<ParcelEvent>, because that would permit adding a different event subtype to the scanned list. The bounded-copy method accepts the two lists safely while keeping ownership of mutation at the destination.
Java code
import java.util.List;
public class ParcelEventCopy {
public interface ParcelEvent { String parcelId(); }
public record ScannedParcel(String parcelId) implements ParcelEvent { }
public static <T extends ParcelEvent> void append(
List<? extends T> source, List<? super T> destination) {
if (source.size() > 47) throw new IllegalArgumentException("Batch exceeds 47 events");
for (T event : source) destination.add(event);
}
}Performance and ownership cost
Reading and appending N elements costs O(N) time and O(1) extra application space. A destination array list may occasionally resize, while a linked destination allocates per entry. The size check is a policy cap, not a rollback mechanism.
Common Mistakes
- Do not treat List<Subtype> as a subtype of List<Supertype>.
- Do not assume a wildcard copy is transactional if a destination add fails.
- Do not use a lower-bounded source when the code needs subtype-specific reads.
Connected lessons
- Java generics: invariance, bounds, and type erasure
- Java collection views: live wrappers, snapshots and shallow copies
- List.copyOf freezes container membership but not mutable elements
- Java type erasure: validate elements with an explicit Class token
- Java bridge methods: preserve overrides after generic erasure
- Java generics and language contracts quiz
- Advanced Java
