Theory Notes on Generic Programming
Variance
Variance describes how subtyping relationships between more complex types (e.g., generics, collections, or function types.) relate to subtyping relationships between their component types. It determines whether a type system allows substitutability in specific contexts, especially in generic programming.
- Invariance: The most restrictive relationship.
If A is a subtype of B,Container<A>andContainer<B>have no relationship at all. - Covariance: Preserves the subtype relationship.
If A is a subtype of B, thenContainer<A>is a subtype ofContainer<B>. - Contravariance: Reverses the subtype relationship.
If A is a subtype of B, thenContainer<A>is a supertype ofContainer<B>.
Covariance and contravariance are closely related to polymorphism in OOP.
For details, see this StackOverflow answer and Eric Lippert’s series of articles.
Invariance
Generics in Java are invariant. Whether or not A and B have an inheritance relationship, Container<A> and Container<B> never do.
- Arrays differ from generic type in two important ways. First arrays are covariant. Generics are invariant.
- Covariant simply means if
Xis subtype ofYthenX[]will also be sub type ofY[]. Arrays are covariant As string is subtype of Object SoString[]is subtype ofObject[]- Invariant simply means irrespective of
Xbeing subtype ofYor not ,List<X>will not be subType ofList<Y>.《Effective Java》 Joshua Bloch
To verify this, consider the following code. If ArrayList<Integer> were a subtype of ArrayList<Number>, the code below would be legal. In fact, the compiler reports an error.import java.util.ArrayList;public class Invariance { public static void main(String[] args) { // Get the Class object of ArrayList ArrayList<Number> list1 = new ArrayList<>(); ArrayList<Integer> list2 = new ArrayList<>(); list1 = list2; }}[ERROR] Type mismatch: cannot convert from ArrayList<Integer> to ArrayList<Number>Java(16777233) [Ln 8, Col 11]
The A.isAssignableFrom(B) method checks whether two classes have an inheritance relationship; it is mainly used inside the JDK source code.
Note that because of type erasure, both ArrayList<Integer> and ArrayList<Number> have the runtime type ArrayList, so they cannot be distinguished at runtime. Checking their relationship with the following code leads to the wrong conclusion.import java.util.ArrayList;public class Invariance { public static void main(String[] args) { // Get the Class object of ArrayList ArrayList<Number> list1 = new ArrayList<>(); ArrayList<Integer> list2 = new ArrayList<>(); if (list1.getClass().isAssignableFrom(list2.getClass())) { System.out.println("list1 is assignable from list2"); } else { System.out.println("list1 is not assignable from list2"); } }}[OUTPUT] list1 is assignable from list2
Covariance
Arrays in Java are covariant. Consider the following code: String is a subtype of Object, so String[] is a subtype of Object[].String[] strings = new String[10];Object[] objects = strings;
Why did the JDK designers make arrays covariant but generics invariant? If you are curious, read https://stackoverflow.com/questions/18666710/why-are-arrays-covariant-but-generics-are-invariant
Contravariance
Contravariance is the reverse of covariance: a generic type over a subtype can stand in for the generic type over its supertype.
There are various reasons why generics were designed to be invariant, but it can feel awkward at first: an apple IS A fruit, yet a container of apples IS NOT A container of fruit.
To use generics more flexibly and correctly, consider wildcards. Pay attention to upper and lower bounds on wildcards, and to the PECS principle.
Erausre
Releated Concepts
Reifiable type
A reifiable type is a type whose type information is fully available at runtime. This includes primitives, non-generic types, raw types, and invocations of unbound wildcards.examples Primitive types int, double, booleanNone-generic String, IntegerRaw types List, SetUnbound wildcard List<?>
Non-reifiable typeexamples parameterized types List<String>, Map<Integer,String>wildcard types List<?>, List<? extends Number>, List<? super Integer>
Tips: reifiable = reify (vt. to make concrete) + able
Heap Pollution
Heap pollution in Java occurs when a variable of a parameterized type refers to an object that is not of that type.
Homogeneous/Heterogeneous translations
Java architect Brian Goetz explained the design philosophy behind generics, and why type erasure was chosen, in Background: How We Got the Generics We Have. In that article he described two approaches to translating parameterised types:
- Homogeneous translation: a generic class
Foo<T>is translated into a single artifact, such asFoo.class(and same for generic methods)- Heterogeneous translation: each instantiation of a generic type or method (
Foo<String>, Foo<Integer>) is treated as a separate entity, and generates separate artifacts.Releated paper:《Two Ways to Bake Your Pizza— Translating Parameterised Types into Java》
Type Erasure in Java
Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming. To implement generics, the Java compiler applies type erasure to:
- Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded.The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.

public class Box<T> { private T value; public void set(T value) { this.value = value; } public T get() { return value; }}public class Foo<T extends Number> { private T value; public void set(T value) { this.value = value; } public T get() { return value; }}public class Box { private Object value; public void set(Object value) { this.value = value; } public Object get() { return value; }}public class Foo { private Number value; public void set(Number value) { this.value = value; } public Number get() { return value; }}- Insert type casts if necessary to preserve type safety.
Box<Integer> integerBox = new Box<>();integerBox.set(10);Integer value = integerBox.get();Box integerBox = new Box();integerBox.set(10);Integer value = (Integer) integerBox.get(); // insert type cast- Generate bridge methods to preserve polymorphism in extended generic types.
Type erasure keeps generics backward compatible, but it can also cause method signature conflicts that break polymorphism. A bridge method is a compiler-generated intermediate method that bridges the parent and child method implementations to preserve polymorphism.class Parent<T> { public T getValue() { return null; }}// Generic subclassclass Child extends Parent<String> { @Override public String getValue() { return "Child Value"; }}// Parent parent = new Child();// Object value = parent.getValue();// Call chain:// parent.getValue()// |// v// Child.bridge$getValue() // bridge method// |// v// Child.getValue() // the actual method returning String// |// v// "Child Value" (converted to Object)public class Main { public static void main(String[] args) { Parent parent = new Child(); Object value = parent.getValue(); // The JVM calls the bridge method, which routes to the subclass method System.out.println(value); }}[OUTPUT] Child Value// Inspect the compiled bytecode$ javap -c Child[OUTPUT]class Child extends Parent<java.lang.String> { Child(); Code: 0: aload_0 1: invokespecial #1 // Method Parent."<init>":()V 4: return public java.lang.String getValue(); Code: 0: ldc #7 // String Child Value 2: areturn public java.lang.Object getValue(); Code: 0: aload_0 1: invokevirtual #9 // Method getValue:()Ljava/lang/String; 4: areturn}
Ref
docs.oracle.com/javase/tutorial/java/generics/erasure.html
docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html
An Expert‘s Guide to Generic Types in Java: Covariance, Contravariance, and Beyond
Java Generics - Bridge method? —— StackOverflow
Java bridge methods explained —— STAS