Theory Notes on Generic Programming

Introduce Variance in Generic Types, Type Erasure, and Related Concepts

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> and Container<B> have no relationship at all.
  • Covariance: Preserves the subtype relationship.
    If A is a subtype of B, then Container<A> is a subtype of Container<B>.
  • Contravariance: Reverses the subtype relationship.
    If A is a subtype of B, then Container<A> is a supertype of Container<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 X is subtype of Y then X[] will also be sub type of Y[]. Arrays are covariant As string is subtype of Object So String[] is subtype of Object[]
  • Invariant simply means irrespective of X being subtype of Y or not ,List<X> will not be subType of List<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.

Wildcards

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.

java_generic

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 typesint, double, boolean
None-genericString, Integer
Raw typesList, Set
Unbound wildcardList<?>

Non-reifiable type

examples
parameterized typesList<String>, Map<Integer,String>
wildcard typesList<?>, 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 as Foo.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