By Obed Rios (5/7/2023)
Revision 1.0
Abstract
In Java, the concepts of variance are related to how the type parameters of a class or interface are related to each other when the class or interface is sub-typed or implemented. The key difference between invariant and covariant in the context of Java generics is how they handle sub-typing relationships. Invariant types do not allow assignments between different type parameters, while covariant types can accept a specified type or any of its sub-types. In addition contravariance enables you to use a more general type (super type) in a generic type or method that would normally require a more specific type (sub-type). In this work, we show explicitly the concepts of variance in the context of Java Generics.
Introduction
Parametric variance refers to the relationship between the type parameters of a class or interface and their subtypes. It defines how subtyping is inherited by the type parameters of a generic class or interface. There are three types of parametric variance: invariant, covariant, and contravariantbloch2017oracle2023.
Invariant: If a class or interface has invariant type parameters, then the subtyping relationship between the class or interface and its subtype is not inherited by the type parameters. In other words, if a class or interface
Foo<T>
is a subtype of another class or interfaceBar<T>
, thenFoo<A>
is not a subtype ofBar<B>
for any typesA
andB
that are not identical.Covariant: If a class or interface has covariant type parameters, then the subtyping relationship between the class or interface and its subtype is inherited by the type parameters. In other words, if a class or interface
Foo<+T>
is a subtype of another class or interfaceBar<+T>
, thenFoo<A>
is also a subtype ofBar<A>
for any typeA
.Contravariant: If a class or interface has contravariant type parameters, then the subtyping relationship between the class or interface and its subtype is reversed for the type parameters. In other words, if a class or interface
Foo<-T>
is a subtype of another class or interfaceBar<-T>
, thenBar<A>
is a subtype ofFoo<A>
for any typeA
.
Invariance and Covariance Worked Example
In this example, we create two lists, intList
and doubleList
, which are of type List<Integer>
and List<Double>
, respectively. Due to the invariant nature of the generic List<E>
class, we cannot directly assign intList
or doubleList
to a variable of type List<Number>
.
The printSum()
method takes a List<? extends Number>
parameter, which uses a wildcard to allow any subtype of Number
as its argument. This allows us to pass both intList
and doubleList
to the method without issues.
In this case, the printList()
method accepts a parameter of type List<? extends Number>
, which allows any sub-type of Number
. This makes the method covariant with respect to its input parameter, allowing it to process List<Integer>
, List<Double>
, and List<Number>
without issues.
Here's a comparison of invariant and covariant using the List<Number>
class:
/**
* Invariant type means that the type parameter remains unchanged. In the context of Java generics, it means that a generic type with a specified type parameter cannot be treated as a generic type with a different type parameter, even if one type is a subtype of another.
*
* Covariant type means that the type parameter can vary in a way that follows the class hierarchy.In Java generics, covariance can be achieved using the ? extends T wildcard, which allows a generic type to accept the specified type or any of its subtypes.
*/
import java.util.ArrayList;
import java.util.List;
public class InvarianceCovarianceExample {
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);
List<Double> doubleList = new ArrayList<>();
doubleList.add(1.1);
doubleList.add(2.2);
doubleList.add(3.3);
// The following lines would cause compile-time errors due to type invariance.
// List<Number> numberList1 = intList;
// List<Number> numberList2 = doubleList;
printSum(intList); // Valid, prints "Sum: 6.0"
printSum(doubleList); // Valid, prints "Sum: 6.6"
// But we cannot pass a List<Number> to printSum() method
List<Number> numberList = new ArrayList<>();
numberList.add(1);
numberList.add(2.0);
numberList.add(3.3);
printSum(numberList);
}
public static void printSum(List<? extends Number> listNumbers) {
double sum = 0;
for (Number number : listNumbers) {
sum += number.doubleValue();
}
System.out.println("Sum: " + sum);
}
public static void printList(List<? extends Number> numbers) {
for (Number number : numbers) {
System.out.print(number + " ");
}
System.out.println();
}
}
Contravariance Worked Example
In this example, we create a custom Consumer<List<? super Number>>
type, which uses a wildcard with a super clause (? super T
) to achieve contravariance. This allows the generic type to accept the specified type or any of its super-types.
We then create a numberConsumer
that takes a List<? super Number>
as an argument and processes the list. The processList()
method takes a List<?>
and a Consumer<List<? super Number>>
and applies the consumer to the list. As a result, we can pass both numberList
and objectList
to the processList()
method without any issues, demonstrating contravariance using a custom functional interface in Java
Here's an example of contravariance using a custom functional interface:
/**
* Contravariance is not directly supported for the List<T> type in Java because List<T> only provides methods that are either invariant or covariant with respect to its type ap arameter. However, we can demonstrate contravariance using a
custom generic functional interface with a single method that takes a List<Number> or any of its supertypes as an argument.
*/
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class ContraVarianceExample {
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
numberList.add(1);
numberList.add(2.0);
numberList.add(3.3);
List<Object> objectList = new ArrayList<>();
objectList.add("Hello");
objectList.add(42);
objectList.add(3.14);
Consumer<List<? super Number>> numberConsumer = list -> {
for (Object item : list) {
System.out.println("Item: " + item);
}
};
processList(numberList, numberConsumer); // Valid
processList(objectList, numberConsumer); // Valid
}
public static void processList(List<?> list, Consumer<List<? super Number>> listProcessor) {
listProcessor.accept((List<? super Number>) list);
}
}
Conclusion
Variance in the context of Java generics allows for more flexible and reusable code by controlling how generic types relate to their type parameters and subtyping relationships. Java supports three kinds of variance through wildcards:
Invariance: By default, Java generics are invariant. This means that a generic type with a specified type parameter cannot be treated as a generic type with a different type parameter, even if one type is a subtype of another. Invariance provides type safety but can be restrictive in some scenarios.
Covariance: Achieved using the
? extends T
wildcard, covariance allows a generic type to accept the specified type or any of its subtypes. This increases the flexibility of generic types and methods, enabling the use of a more specific type (a subtype) in place of a more general type (a super type).Contravariance: Achieved using the
? super T
wildcard, contravariance allows a generic type to accept the specified type or any of its super types. This further increases the flexibility of generic types and methods, enabling the use of a more general type (a super type) in place of a more specific type (a subtype).
Understanding and properly using variance in Java generics can lead to cleaner, more adaptable, and more robust code. It's essential to choose the right kind of variance for a particular use case to ensure that your code is flexible and type-safe at the same time.
References
- [bloch2017] Bloch, J. (2017). Effective Java (3rd ed.). Addison-Wesley.
- [oracle2023] Oracle Java Generics. Retrieved from https://docs.oracle.com/javase/tutorial/java/generics/index.html
Comments
Post a Comment