Sitemap
Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

Java generics (in)variance.

7 min readAug 24, 2025

--

Developers create objects and their descendants or ancestors in this way.

Generics is a mechanism for creating generic classes, interfaces, and methods that work with different data types. It helps avoid code duplication while providing compiler-level type safety.

Generics works with reference types, not with primitives like int, long:

private <T> void printMyArray(T[] ary) {
for (T item: ary) System.out.println(item);
}
Integer[] source = {1,2,3,4,5};
printMyArray(source);

Extends — a keyword in the Java language that is used to establish an inheritance relationship between two classes. It allows a derived class to inherit the properties and behavior (fields and methods) of another class.

  • Code reuse. Subclasses can use methods and variables of the superclass.
  • Method overriding. A subclass can override methods of a superclass.
  • Establishing an “is-a” relationship. For example, “Cat is an Animal.”

class Animal {
private String name;
Animal(String name) {
this.name = name;
}
}
class Cat extends Animal {
Cat(String name) {
super(name);
}
}

Super — is a keyword in the Java language that is used to refer to the super class or parent class.

  • Accessing members of a superclass when the subclass also has members with the same name.
  • Calling superclass constructors by default or with parameters.
  • Calling superclass methods in a subclass if the subclass has overridden the methods.
  • The super keyword cannot be used in static methods or in any other static context.

1. Contravariance

Contravariance for the List type in Java is a property that allows collections to include a superclass of a given type, but to retrieve elements of type T from the collection without explicit casting. It works well in method arguments, not in return types. Pointless to create a List<? super String> and expect to get anything except a String type. If you want a method that takes a List<? super E>, here's how you could do that:

// Object as supertype always ready to get the subtype String.
List<? super String> list = new ArrayList<Object>();
List<? super Number> nums = new ArrayList<Number>();

An entry of the form “? extends…” or “? super…” is a wildcard character, with an upper bound (extends) or with a lower bound (super).

  • <? super T> is contravariant: you can pass in a list of T or any of its supertypes.
  • You can write elements of type T into List<? super T>, but reading returns Object.
  • In contrast, <? extends T> is covariant and allows safe reading, but writing is limited.
  • Contravariance is associated with the reversal of the hierarchy of original types in derived types. For example: List<? super Number> is a super type of List<Number>, List<Object> и List<? super Object>.
  • Contravariance allows the type to be set correctly when creating subtypes, but does not allow type checking to be bypassed. This is important, for example, if the container is used only for writing to it, and there is no reading.
import java.util.*;
class Test {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
contravariance(list);
list.forEach(s -> System.out.println(s));
}
private static void contravariance(List<? super Number> contra) {
contra.add(100); // Allows to use int type
//contra.add("T"); //incompatible types: String cannot be converted
}
}

Contravariance prevents you from:

  • Retrieving elements of type T from a collection without an explicit cast. For example, if List<? super Number> contains objects of type Number, you cannot retrieve elements of type Integer from the list without a cast.
  • Adding elements of any type to a list referenced via a wildcard. For example, if you have a variable of type List<? extends Number>, trying to add an object of any type will fail because the reference could be to an object of type List<Integer>, List<BigDecimal>, or even List<Number>.

Contravariance allows girlfriend to accept any type of flower:

import java.util.*;
class Flower { }
class Rose extends Flower { }
class Daisy extends Flower { }

interface Girl<T extends Flower> {
<T extends Flower> void takeGift2(T flower);
}
class Lover implements Girl<Flower> {
@Override
public <T extends Flower> void takeGift2(T flower) {
System.out.println("I like " + flower.getClass().getName());
}
}
class Main {
public static void main(String[] args) {
Girl<? super Rose> girl = new Lover();
girl.takeGift2(new Daisy());
}
}

2. Invariance

Type mismatch invariance in Java Generics is a rule that prohibits assigning a reference to an object of one type to a variable of another type, even if both are instances of the same type. This is due to type erasure — at compile time, the compiler removes information about generic types from the bytecode, and information about them is lost during program execution.

  • Subclass or superclass assignments are not allowed. This is because the compiler cannot guarantee what element type was originally specified for the collection.
  • Even if one type is a subtype of another, the generic types of those types do not preserve the inheritance hierarchy. For example, if String is a subtype of CharSequence, then Set<String> is not considered a subtype of Set<CharSequence>.
//Use restricted type parameters - this restricts the types that can be used
//with the generic type. For example, if a generic is declared

class Box<T extends Number> {
Box<Long> longBox = new Box<>();
Box<Integer> intBox = new Box<>();
Box<String> stringBox = new Box<>(); // incompatible types

3. Covariance

Assigning a reference to an object of a superclass type to a reference to an object of a subclass. In Java, this is typical for arrays, since they are covariant:

Object[] objects = new String[10]; // Object is a supertype in Java

// but if we use not the array:
List<String> strings = new ArrayList<Object>();
// incompatible types: ArrayList<Object> cannot be converted to List<String>

Java does not support covariance for Lists. This limitation is due to the nature of generics in Java. Generics in Java are invariant — types must be identical, subclass or superclass assignments are prohibited. This solution is related to type erasure — the compiler does not “remember” the specific type used to create an instance on the right side of an assignment. This avoids errors associated with assigning a list of children to a list of parents and vice versa.

List<Integer> ints = new ArrayList<Integer>();.
List<? extends Number> nums = ints;
// List<Integer> - is a subtype of List<? extends Number>.

Java allows covariance for generic collection types, but only with explicit permission from the developer. For this purpose, a special syntax is provided — a wildcard, specifying the constraints:

// limited by myType and and its descendants;
List<? extends myType>
// limited by myType and and its ancestors;
List<? super myType>

However, the use of such constructions is limited. For example, a list of the form List<? extends myType> does not allow adding elements — the compiler cannot determine which of the descendants of myType it is actually typed by. If the exception is null, it can be used as any type.

class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Madrid");
list.add(null);
list.add("");
var result = checkForData(list);
result.forEach(s -> System.out.println(s));
}
static <T extends CharSequence> List<Object> checkForData(List<T> nums) {
List<Object> result = new ArrayList<>();
for (T item : nums) {
if (item != null && item.length() > 0) {
result.add(item);
}
}
return result;
}
}

4. Type erasure

Type erasure is the process by which the type information of parameterized generic types is removed at compile time and is not preserved during program execution.

Get Aleksei Jegorov’s stories in your inbox

Join Medium for free to get updates from this writer.

No information about its parameter type is stored inside the class. This information is only available at compile time and is erased (becomes unavailable) at runtime. The purpose of type erasure is to ensure backward compatibility with code written before parameterized types were introduced in Java.

  • Sample<T> → becomes Sample at runtime.
  • All T are erased to Object.
  • So at runtime, Sample<String> and Sample<Integer> are the same class: Sample.
import java.util.*;
class Sample<T> {
private T value;
public Sample(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public <U extends Number> void printDouble(U number) {
System.out.println(number.doubleValue());
}
}
class Main {
public static void main(String[] args) {
Sample<Integer> intSample = new Sample<>(1001);
System.out.println(intSample.getValue());
Sample<Character> sample = new Sample<>('a');

intSample.printDouble(10); // 10.0
sample.printDouble(99.9); // 99.9
System.out.println(sample.getClass()==intSample.getClass()); //true
}
}

Generic sample for checking null.

public <T> T checkNotNull(T item) {
if (item == null) {
throw new NullPointerException();
}
return item;
}
void myTreatMethod(int a, Person p) {
// we can break it at once without alternative
// Objects.requireNonNull(p, "Person must not be null");
if (checkNotNull(p).getAge() > a) {
System.out.println("Person is older than " + a);
}
else {
System.out.println("Person is not older than " + a);
}
}
public static void main(String[] args) {
Person p = new Person(25);
myTreatMethod(20, p);
try {
p.checkAdult();
} catch (PersonTooYoungException ex) {
System.out.println("Exception: " + ex.getMessage());
}
}
public class Person {
private final int age;
public Person(int age) {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
this.age = age;
}
public int getAge() {
return age;
}
public void checkAdult() {
if (age < 18) {
throw new PersonTooYoungException("Person must be an adult");
}
}
}
class PersonTooYoungException extends RuntimeException {
public PersonTooYoungException(String message) {
super(message);
}
}

Alternative: use Optional instead of:

public void myTreatMethod(int a, Optional<Person> maybePerson) {
maybePerson
.map(Person::getAge)
.filter(age -> age > a)
.ifPresent(age -> {
...
});
}
//or using Lombok library to reduce any boilerplate code:
public void myTreatMethod(int a, @NonNull Person p) {
if (p.getAge() > a) {
...
}
}

Conclusion

Covariance assumes a suitable choice of a more specific type, invariance requires an exact match of types, and contravariance accepts more general types via substitution.

--

--

Aleksei Jegorov

Written by Aleksei Jegorov

Java, Kotlin, T-SQL, Angular2, Typescript, ReactJs - developer in Estonia, Tallinn. “Code is like humor. When you have to explain it, it’s bad.” - Cory House

Dev Genius

Published in Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development