Generics let declarations express relationships between types so the compiler can reject incompatible values before execution.
Java generics: invariance, bounds, and type erasure
Parameterise the contract
A List<Invoice> states what the list accepts and what its reads return. A raw List discards that relationship and moves errors toward runtime casts. Generic type checking does not validate business rules such as invoice currency or an allowed amount range.
List<Integer> is not a subtype of List<Number>. If it were, a caller receiving the supposed Number list could insert a Double into an Integer list. This is invariance, and it protects writes.
Arrays behave differently: reference arrays are covariant and perform runtime store checks. Do not apply the array assignment rule to a generic list. It describes a different safety model.
Use a bound for the direction of data
A source declared as List<? extends Number> can provide Number values, but you cannot safely add an arbitrary Number to it. Its actual element type might be Integer. A destination declared as List<? super Integer> can accept Integer values, while a read can only promise Object.
The program transfers integers into a Number destination. The upper-bounded source supplies compatible input and the lower-bounded destination accepts the copied integers. This keeps callers flexible without resorting to raw types.
Most generic information is erased from ordinary runtime object types. You cannot use new T(), create a normal new List<String>[] array, or distinguish List<String> from List<Integer> with an instanceof test.
Working program
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class TypedBatchTransfer {
static void transfer(List<? extends Integer> source, List<? super Integer> destination) {
for (Integer shipmentCount : source) {
destination.add(shipmentCount);
}
}
public static void main(String[] args) {
List<Integer> dailyCounts = Arrays.asList(4, 7, 2);
List<Number> report = new ArrayList<>();
transfer(dailyCounts, report);
System.out.println(report);
}
}Output
[4, 7, 2]Cost and design choices
The transfer visits n source elements and appends n references. With an ArrayList destination, total append work is amortized O(n), and the destination needs O(n) additional reference storage. Generic bounds do not eliminate allocation or boxing.
List<Integer> stores references to boxed values, not an int array. For large numeric workloads, a primitive array or primitive stream can avoid some wrapper overhead.
Erasure does not make generic declarations useless: it places their main safety checks at compilation. Keep collection contracts typed all the way to the boundary, and validate untrusted runtime data separately.
Common Mistakes
- Do not use raw types to silence a type mismatch.
- Do not assume a wildcard source accepts writes of its upper bound.
- Do not cast a list to a different element type and call that conversion.
Connect the contracts
Compare the boundary explained in Generic function types with the assumptions made by this program.
Apply this contract in Spring
Spring ObjectProvider: deferred resolution without hiding ownership. These lessons keep framework assembly separate from the Java contract.
Related node and runtime checks
Continue with Java generic varargs: audit the hidden array before using SafeVarargs.
Continue with: Java generic wildcards: copy from a producer into a consumer, Java type erasure: validate elements with an explicit Class token, Java bridge methods: preserve overrides after generic erasure.
